Productivity & Home
Nextcloud logo

How to install Nextcloud with Docker

Nextcloud cover: How to install Nextcloud with Docker
Difficulty: IntermediateTime: 40 minutesTested with: Nextcloud 35 (official image)

Nextcloud is the free, open-source, self-hosted alternative to Google Drive, Dropbox and OneDrive, and then some. File sync and sharing with desktop and phone apps, calendars, contacts, notes, tasks and hundreds of add-on apps, all on your own server. Pair it with ONLYOFFICE (see my ONLYOFFICE guide) and you get online document editing too.

In this guide we will run Nextcloud with Docker Compose using the official Docker Hub image, following the project's own nextcloud/docker examples: Nextcloud with Apache, MariaDB, Redis for caching and file locking, and a dedicated cron container for background jobs.

💡
Note: This guide works with the current Nextcloud 35 release (35.0.1 at the time of writing). I pin the major version (nextcloud:35-apache) because Nextcloud can only be upgraded one major version at a time.
💡
Which Nextcloud? This image is maintained by the community. If you want Nextcloud to manage its own containers, backups and Office for you, look at Nextcloud All-in-One (AIO), the officially recommended "easy" option. If you would rather not manage Docker at all, Nextcloud's install page also lists ready-made appliances (VM images and partner hardware). This guide is for people who want to see and control every piece.

✅ Before you start

  • A Linux host with Docker and Docker Compose v2 installed (docker compose version should work). Use docker compose with a space: the old Python docker-compose v1 can crash on modern Docker.
  • At least 2 GB of RAM and plenty of disk for your files.
  • A domain name and a reverse proxy with HTTPS. Nextcloud really wants to run over HTTPS, and the phone apps need it.
  • One free port, 8081, for the web UI before the proxy is in place.

STEP 1: User and group IDs (and your secrets)

The official Nextcloud image does not use PUID/PGID. Apache runs as www-data (user and group 33) and the image sets up its own folder permissions on first start.

What you do need are three passwords: one for the MariaDB root user, one for the Nextcloud database user, and one for your Nextcloud admin account. Generate them like this (run it three times):

openssl rand -base64 24

Keep them somewhere safe, such as your password manager. In the examples below they appear as placeholders like CHANGE_ME_STRONG_PASSWORD and change-me.

STEP 2: Create the folders

I keep every app's files under one parent folder so a backup is a single job. Create the folders for Nextcloud:

mkdir -p /path/to/appdata/nextcloud/{html,db}

When you are done, the layout should look like this:

/path/to/appdata/nextcloud/
    docker-compose.yml
    db.env                        # database credentials
    html/                         # Nextcloud itself, config.php, apps and (by default) your files
    db/                           # the MariaDB database

Folder layout

Finally, I give every web app that sits behind my reverse proxy one shared Docker network called proxy. Nextcloud joins it so Traefik or Nginx Proxy Manager can reach it by container name, while its database and helpers stay on the stack's own private network. Create it once (if it already exists, Docker just tells you so):

docker network create proxy

STEP 3: The compose file

First create /path/to/appdata/nextcloud/db.env with the database details. Both the database and Nextcloud read it, so the values only live in one place:

MYSQL_PASSWORD=CHANGE_ME_STRONG_PASSWORD
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud

db.env

Create /path/to/appdata/nextcloud/docker-compose.yml and paste in the following. It is the official MariaDB + Apache example from nextcloud/docker, with bind mounts, the admin account, trusted domains and the shared proxy network added. Use the copy button in the top right corner of the block.

services:
  db:
    image: mariadb:lts
    container_name: nextcloud-db
    restart: always
    command: --transaction-isolation=READ-COMMITTED
    volumes:
      - /path/to/appdata/nextcloud/db:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=CHANGE_ME_STRONG_PASSWORD   # MariaDB root password
      - MARIADB_AUTO_UPGRADE=1
      - MARIADB_DISABLE_UPGRADE_BACKUP=1
    env_file:
      - db.env

  redis:
    image: redis:alpine
    container_name: nextcloud-redis
    restart: always

  app:
    image: nextcloud:35-apache
    container_name: nextcloud
    restart: always
    ports:
      - "8081:80"
    volumes:
      - /path/to/appdata/nextcloud/html:/var/www/html
    environment:
      - MYSQL_HOST=db
      - REDIS_HOST=redis
      - NEXTCLOUD_ADMIN_USER=admin
      - NEXTCLOUD_ADMIN_PASSWORD=CHANGE_ME_STRONG_PASSWORD
      - NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com 192.168.1.20
      - TRUSTED_PROXIES=172.16.0.0/12 192.168.1.0/24
      - OVERWRITEPROTOCOL=https
      - APACHE_DISABLE_REWRITE_IP=1
    env_file:
      - db.env
    depends_on:
      - db
      - redis
    networks:
      - default
      - proxy

  cron:
    image: nextcloud:35-apache
    container_name: nextcloud-cron
    restart: always
    volumes:
      - /path/to/appdata/nextcloud/html:/var/www/html
    entrypoint: /cron.sh
    depends_on:
      - db
      - redis

networks:
  proxy:
    external: true

docker-compose.yml

STEP 4: The settings explained

Setting What it does
mariadb:lts The database, on the current long-term-support release. --transaction-isolation=READ-COMMITTED is what Nextcloud requires.
MARIADB_AUTO_UPGRADE Lets MariaDB upgrade its system tables itself when the image moves to a newer version.
db.env Database name, user and password, read by both db (to create them) and app (to connect).
redis Memory cache and transactional file locking. The image configures Nextcloud to use it automatically because REDIS_HOST is set.
nextcloud:35-apache Nextcloud 35 with Apache built in. It follows 35.x updates but never jumps a major version on its own.
/var/www/html Nextcloud's code, config/config.php, installed apps and, by default, everyone's files (in data/). Back up this folder and the database.
NEXTCLOUD_ADMIN_USER / _PASSWORD Creates the admin account and runs the installer automatically on the first start. Only used once.
NEXTCLOUD_TRUSTED_DOMAINS Space-separated list of hostnames Nextcloud will answer to. Add your proxy hostname and the host IP.
TRUSTED_PROXIES The addresses of your reverse proxy, so Nextcloud trusts its forwarded headers. 172.16.0.0/12 covers Docker networks; adjust to your setup.
OVERWRITEPROTOCOL Tells Nextcloud that users really reach it over https, even though the proxy talks to it over plain HTTP.
APACHE_DISABLE_REWRITE_IP Stops Apache rewriting the client IP itself, so Nextcloud reads the forwarded host, protocol and client IP from the proxies listed in TRUSTED_PROXIES, as the image docs describe.
cron A second container from the same image that runs /cron.sh: Nextcloud's background jobs every five minutes.
👍
Tip: Prefer PostgreSQL?
The nextcloud/docker repo has a Postgres version of this example: swap db for postgres:alpine (mount /var/lib/postgresql) and use POSTGRES_HOST, POSTGRES_DB, POSTGRES_USER and POSTGRES_PASSWORD instead of the MYSQL_* variables. Both work well.

STEP 5: Start Nextcloud

From the folder containing your compose file:

cd /path/to/appdata/nextcloud
docker compose up -d
docker compose logs -f

Wait until the logs settle down (press Ctrl+C to stop following them; the container keeps running), then check it is up:

docker ps --filter name=nextcloud

You should see nextcloud, nextcloud-db, nextcloud-redis and nextcloud-cron. The first start copies Nextcloud into html/ and runs the installer, which can take a few minutes. Follow the logs until Apache reports it is ready to serve requests.

STEP 6: First-run setup

  1. Open http://192.168.1.20:8081 and log in as admin with the password from the compose file. (Once the proxy is in place, always use your HTTPS hostname instead.)
  2. Go to Administration settings → Basic settings → Background jobs and select Cron. The cron container is already running the jobs; this tells Nextcloud to rely on it instead of running jobs on page loads (AJAX).
  3. Open Administration settings → Overview. Nextcloud runs security and setup checks there; work through anything it flags.
  4. Create user accounts under Accounts, then install the desktop and phone apps.
  5. Browse Apps for Calendar, Contacts, Notes, Deck and the ONLYOFFICE connector.

You will sometimes need Nextcloud's command-line tool, occ. Always run it as the web server user:

docker compose exec -u 33 app php occ status
docker compose exec -u 33 app php occ maintenance:repair --include-expensive
docker compose exec -u 33 app php occ db:add-missing-indices

STEP 7: Put Nextcloud behind a reverse proxy

Use a dedicated hostname such as cloud.example.com (it must be in NEXTCLOUD_TRUSTED_DOMAINS). Uploads can be large, so make sure the proxy does not cap the request size.

Option A: Traefik (Docker labels)

If Traefik runs on the same Docker host and is attached to the proxy network, add these labels to the nextcloud service. They assume your HTTPS entry point is called websecure and your certificate resolver is called cloudflare; change them to match your Traefik config.

    labels:
      - traefik.enable=true
      - traefik.http.routers.nextcloud.rule=Host(`cloud.example.com`)
      - traefik.http.routers.nextcloud.entrypoints=websecure
      - traefik.http.routers.nextcloud.tls.certresolver=cloudflare
      - traefik.http.services.nextcloud.loadbalancer.server.port=80
      - traefik.docker.network=proxy

docker-compose.yml (extra lines)

Option B: Traefik (file provider)

If Traefik runs on a different machine, add a router and service to your dynamic config file instead:

http:
  routers:
    nextcloud:
      rule: Host(`cloud.example.com`)
      entryPoints:
        - websecure
      tls:
        certResolver: cloudflare
      service: nextcloud
  services:
    nextcloud:
      loadBalancer:
        servers:
          - url: http://192.168.1.20:8081

dynamic.yml

Option C: Nginx Proxy Manager

Add a Proxy Host for cloud.example.com with scheme http, forward hostname nextcloud (if NPM is on the proxy network) or 192.168.1.20, and forward port 80 (or 8081 if you use the host IP). Request a certificate on the SSL tab and tick Force SSL. Under the Advanced tab add client_max_body_size 0; and proxy_buffering off; so big uploads work.

Put the labels on the app service. For CalDAV/CardDAV discovery (the "well-known" URLs), add a redirect for /.well-known/carddav and /.well-known/caldav to /remote.php/dav/ in your proxy; the Nextcloud overview page tells you if it is missing. The reverse proxy docs cover the details.

Once everything goes through the proxy, change the port mapping to "127.0.0.1:8081:80" so Nextcloud is not reachable over plain HTTP from the LAN.

Updating Nextcloud

Within a major version (35.x), updating is a pull and restart. The image runs the upgrade for you on start:

cd /path/to/appdata/nextcloud
docker compose pull
docker compose up -d
docker image prune -f
⚠️
Warning: Major upgrades go one step at a time. To move from 35 to 36, change both image: lines to nextcloud:36-apache, pull and start, and wait for the upgrade to finish before going further. Never skip a version (e.g. 34 to 36). Back up html/ and the database first, and check that your apps support the new version. Do not use the web updater inside Nextcloud with Docker.

Troubleshooting

"Access through untrusted domain". The hostname is not in the trusted list. Add it with docker compose exec -u 33 app php occ config:system:set trusted_domains 2 --value=cloud.example.com. (NEXTCLOUD_TRUSTED_DOMAINS only applies on the first install.)

Login loops, or links point to http://. OVERWRITEPROTOCOL=https or TRUSTED_PROXIES is missing or wrong. Check config/config.php in html/.

"Last background job execution ran ... ago". The cron container is not running, or Background jobs is not set to Cron. Check docker logs nextcloud-cron.

Uploads fail with 413. Your proxy limits body size. Remove the limit as shown in STEP 7. (APACHE_BODY_LIMIT controls Apache's own limit inside the container.)

Everything is slow. Redis is not being used. Check that REDIS_HOST=redis is set and look for memcache.locking in config.php.

That's it! Your own cloud, on your own hardware, with nobody scanning your files. ☁️