Tutorials  /  Docker

Run Nextcloud with Docker Compose

LLudwig · August 2026 ·9 min read ·Docker, Tutorial

A single docker run nextcloud container is fine for a quick look, but it falls back to SQLite, has no cache, and never runs background jobs. A usable instance needs at least four services: the app, a database, Redis for transactional file locking, and a cron runner. This tutorial builds that stack with Docker Compose and covers the configuration the web installer does not handle.

Prerequisites

  • A Linux host with Docker Engine 24 or newer and the Compose v2 plugin (docker compose version)
  • A user account in the docker group or with sudo rights
  • At least 2 GB RAM and enough disk space for user data; a scalable Linux cloud VM with a separate data volume is a practical base
  • A DNS record pointing at the host if you want TLS and external access

Check the Compose plugin before you start:

Console
$ docker compose version
Docker Compose version v2.29.7

Why run Nextcloud as a multi-container stack?

Nextcloud runs as a multi-container stack because the official image ships only PHP and a web server: the database, the Redis cache used for transactional file locking, and the background job runner each need their own container.

The four services split like this:

Service Image Role
app nextcloud:31-apache PHP application and web server
db mariadb:11.4 Persistent database
redis redis:7-alpine File locking and memory cache
cron nextcloud:31-apache Runs /cron.sh every 5 minutes
graph TD
  A["Browser"] --> B["Reverse proxy on the host (TLS, port 443)"]
  B --> C["app: nextcloud:31-apache on 127.0.0.1:8080"]
  C --> D["db: MariaDB 11.4"]
  C --> E["redis: file locking and cache"]
  C --> F["Volume nextcloud: /var/www/html"]
  G["cron: /cron.sh"] --> D
  G --> F

The cron container mounts the same application volume as app and shares the database. It exists so background jobs do not depend on someone opening the web interface.

VM

Matching infrastructure at centron

No hardware needed to follow along: ccloud³ VMs with full root access, billed by the hour and ready in seconds. Rent a cloud server →

Which Nextcloud image tag should you use?

Use a tag that pins the major version, such as nextcloud:31-apache, because Nextcloud refuses to skip a major release during upgrade and a floating tag can jump two majors at once, leaving the instance unable to start.

The -apache variant contains a working web server and is the shortest path to a running instance. The -fpm variant expects a separate nginx container and a shared volume, which adds a service without changing what Nextcloud can do. Start with -apache and terminate TLS in a reverse proxy on the host.

Create the project directory and secrets

Keep the compose file and the environment file in one directory. Nothing else belongs there; all persistent data lives in named volumes.

Console
$ sudo mkdir -p /srv/nextcloud
$ sudo chown $USER:$USER /srv/nextcloud
$ cd /srv/nextcloud
$ openssl rand -base64 24

Generate one password per secret and write them into /srv/nextcloud/.env:

ini
MARIADB_ROOT_PASSWORD=CHANGE_ME_ROOT
MARIADB_PASSWORD=CHANGE_ME_DB
REDIS_PASSWORD=CHANGE_ME_REDIS
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=CHANGE_ME_ADMIN
NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com
OVERWRITECLIURL=https://cloud.example.com

Restrict the file so it is not world-readable:

Console
$ chmod 600 /srv/nextcloud/.env

Write the compose file

Create /srv/nextcloud/docker-compose.yml. Every credential comes from .env, and the app container binds only to the loopback interface because the reverse proxy handles public traffic.

yaml
services:
  db:
    image: mariadb:11.4
    restart: unless-stopped
    command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW
    volumes:
      - db:/var/lib/mysql
    environment:
      MARIADB_AUTO_UPGRADE: "1"
      MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
      MARIADB_DATABASE: nextcloud
      MARIADB_USER: nextcloud
      MARIADB_PASSWORD: ${MARIADB_PASSWORD}
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis:/data
  app:
    image: nextcloud:31-apache
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    depends_on:
      - db
      - redis
    volumes:
      - nextcloud:/var/www/html
    environment:
      MYSQL_HOST: db
      MYSQL_DATABASE: nextcloud
      MYSQL_USER: nextcloud
      MYSQL_PASSWORD: ${MARIADB_PASSWORD}
      REDIS_HOST: redis
      REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
      NEXTCLOUD_ADMIN_USER: ${NEXTCLOUD_ADMIN_USER}
      NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_ADMIN_PASSWORD}
      NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_TRUSTED_DOMAINS}
      OVERWRITEPROTOCOL: https
      OVERWRITECLIURL: ${OVERWRITECLIURL}
      TRUSTED_PROXIES: 172.16.0.0/12
      PHP_MEMORY_LIMIT: 1G
      PHP_UPLOAD_LIMIT: 10G
  cron:
    image: nextcloud:31-apache
    restart: unless-stopped
    entrypoint: /cron.sh
    depends_on:
      - db
      - redis
    volumes:
      - nextcloud:/var/www/html
volumes:
  db:
  redis:
  nextcloud:

Three settings are worth naming explicitly:

  • --transaction-isolation=READ-COMMITTED: Nextcloud requires this isolation level on MySQL and MariaDB. Without it, the admin overview reports a database warning and deadlocks become more likely under concurrent uploads.
  • TRUSTED_PROXIES: without the proxy network range, every request appears to come from the Docker bridge address and rate limiting or brute-force protection blocks the wrong client.
  • PHP_UPLOAD_LIMIT: this value must match client_max_body_size in the reverse proxy, otherwise large uploads fail at the proxy before PHP ever sees them.

Start the stack and verify

Bring the stack up and watch the first start. The app container installs Nextcloud on the empty volume, which takes about a minute.

Console
$ cd /srv/nextcloud
$ docker compose up -d
$ docker compose logs -f app

Wait for Initializing finished in the log, then check the state of all four services and the installation itself:

Console
$ docker compose ps
$ docker compose exec -u www-data app php occ status

Expected output:

Console
  - installed: true
  - version: 31.0.5.1
  - versionstring: 31.0.5
  - maintenance: false
  - needsDbUpgrade: false

A final check against the status endpoint confirms the app answers HTTP requests on the loopback port:

Console
$ curl -s http://127.0.0.1:8080/status.php
{"installed":true,"maintenance":false,"needsDbUpgrade":false,"productname":"Nextcloud"}

How do you run occ commands in a container?

Run occ commands with docker compose exec -u www-data app php occ <command>, because the Nextcloud command line tool must run inside the app container as the web server user www-data, not on the Docker host.

Two commands you will need right after installation:

Console
$ docker compose exec -u www-data app php occ background:job:mode cron
$ docker compose exec -u www-data app php occ config:system:get trusted_domains

The first switches background jobs from AJAX to cron, which is what the cron container serves. The second prints the domain list; NEXTCLOUD_TRUSTED_DOMAINS is only evaluated during the initial installation, so later domain changes go through occ:

Console
$ docker compose exec -u www-data app php occ config:system:set trusted_domains 1 --value=cloud.example.com

Put a reverse proxy in front

Terminate TLS on the host and forward plain HTTP to 127.0.0.1:8080. This nginx server block covers the headers Nextcloud checks and the two service discovery redirects that clients expect:

nginx
server {
    listen 443 ssl http2;
    server_name cloud.example.com;
    ssl_certificate     /etc/letsencrypt/live/cloud.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cloud.example.com/privkey.pem;
    client_max_body_size 10G;
    location = /.well-known/carddav { return 301 /remote.php/dav; }
    location = /.well-known/caldav  { return 301 /remote.php/dav; }
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_request_buffering off;
        proxy_read_timeout 3600s;
    }
}

Reload nginx and open the domain. The admin account from .env is already created, so you land on the login screen rather than the installer.

Back up and update

A backup is only consistent if the database dump and the nextcloud volume are taken at the same point in time. Enable maintenance mode for the duration:

Console
$ docker compose exec -u www-data app php occ maintenance:mode --on
$ docker compose exec db mariadb-dump --single-transaction -u nextcloud -p nextcloud > /srv/backup/nextcloud-db.sql
$ docker run --rm -v nextcloud_nextcloud:/data -v /srv/backup:/backup alpine tar czf /backup/nextcloud-data.tar.gz -C /data .
$ docker compose exec -u www-data app php occ maintenance:mode --off

The volume name is the project directory name plus the volume key, so /srv/nextcloud produces nextcloud_nextcloud. Confirm it with docker volume ls before scripting the command. Backup runs, cron execution and container restarts are worth collecting centrally, for example through Advanced Monitoring.

For updates, raise the tag one major version at a time in docker-compose.yml, for example from nextcloud:31-apache to nextcloud:32-apache, and change it in both app and cron:

Console
$ docker compose pull
$ docker compose up -d
$ docker compose exec -u www-data app php occ status

The entrypoint of the new image runs the upgrade automatically. If needsDbUpgrade stays true, trigger it manually with docker compose exec -u www-data app php occ upgrade.

Troubleshooting

"Access through untrusted domain": the domain is missing from trusted_domains. The environment variable no longer applies after installation, so set the entry with occ config:system:set trusted_domains 1 --value=<your-domain>.

Redis error NOAUTH Authentication required: REDIS_HOST_PASSWORD in the app container does not match --requirepass in the redis service. Both read the same .env variable, so this usually means the app container was started before the value existed. Run docker compose up -d --force-recreate app cron.

Background jobs show as overdue: check that the cron container is running with docker compose ps cron and that the job mode is cron. The container executes /cron.sh every 5 minutes; the admin overview flags a delay only after 10 minutes without a run.

Next steps

The stack now covers the application, database, cache and background jobs, with all state in three named volumes. Verify a restore of nextcloud-db.sql and nextcloud-data.tar.gz on a second host once, before you rely on the backup. From there, the useful additions are object storage as a primary storage backend and a separate container for full text search.

More on Docker

Jetzt 200 € Guthaben sichern

Testen Sie Ihr Setup auf ccloud³

Registrieren Sie sich in der ccloud³ und erhalten Sie 200 € Startguthaben für Ihr Projekt – z. B. für eine PostgreSQL-VM mit automatischen Backups.

Ludwig Technische Redaktion

Schreibt bei centron über Linux-Administration, Container und Datenbanken – mit Fokus auf Anleitungen, die im Betrieb tatsächlich funktionieren.

Kategorie Docker
Teilen
Noch offene Fragen?

Unser Team hilft Ihnen bei Ihrem konkreten Setup weiter – von Menschen, die die Plattform selbst betreiben.

War dieses Tutorial hilfreich?

Ihre Antwort wird anonym gespeichert und hilft uns, die Tutorials zu verbessern.

Kommentare

Noch keine Kommentare – stellen Sie die erste Frage zu diesem Tutorial.

Zum Kommentieren anmelden

Kommentare stehen centron-Kunden offen. Melden Sie sich in Ihrem Konto an, um eine Frage zu diesem Tutorial zu stellen.

Weiterlesen

Das könnte Sie auch interessieren

Jetzt kostenlos anfangen

Melden Sie sich an und erhalten Sie in den ersten 60 Tagen ein Guthaben von 200 € bei centron.

Dieses Werbeangebot gilt nur für neue Konten. Angebot ausschließlich für Gewerbetreibende.

Jetzt loslegen Sales kontaktieren