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
dockergroup or withsudorights - 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:
$ docker compose version
Docker Compose version v2.29.7Why 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.
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.
$ sudo mkdir -p /srv/nextcloud
$ sudo chown $USER:$USER /srv/nextcloud
$ cd /srv/nextcloud
$ openssl rand -base64 24Generate one password per secret and write them into /srv/nextcloud/.env:
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.comRestrict the file so it is not world-readable:
$ chmod 600 /srv/nextcloud/.envWrite 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.
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 matchclient_max_body_sizein 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.
$ cd /srv/nextcloud
$ docker compose up -d
$ docker compose logs -f appWait for Initializing finished in the log, then check the state of all four services and the installation itself:
$ docker compose ps
$ docker compose exec -u www-data app php occ statusExpected output:
- installed: true
- version: 31.0.5.1
- versionstring: 31.0.5
- maintenance: false
- needsDbUpgrade: falseA final check against the status endpoint confirms the app answers HTTP requests on the loopback port:
$ 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:
$ 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_domainsThe 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:
$ docker compose exec -u www-data app php occ config:system:set trusted_domains 1 --value=cloud.example.comPut 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:
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:
$ 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 --offThe 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:
$ docker compose pull
$ docker compose up -d
$ docker compose exec -u www-data app php occ statusThe 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
- Run Ollama in a Docker Container
- Encrypted Docker Volume Backups with Restic
- Shrink Docker Images with Multi-Stage Builds
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.