Nextcloud besteht nicht aus einem Dienst, sondern aus mindestens vier: PHP-Anwendung, Datenbank, Cache und ein Cron-Prozess für Hintergrundjobs. Docker Compose bündelt diese Teile in einer Datei, macht die Versionen explizit und das Setup reproduzierbar.
In dieser Anleitung baust du einen vollständigen Stack aus Nextcloud, MariaDB, Redis, einem Cron-Container und Caddy als TLS-Terminierung, prüfst die Installation über occ und richtest ein Backup ein.
Warum reicht ein einzelner Nextcloud-Container nicht?
Ein einzelner Nextcloud-Container liefert nur PHP und Apache, aber weder Datenbank noch Cache, noch den Hintergrund-Cron und die TLS-Terminierung, die eine produktive Instanz zwingend braucht. Das mitgelieferte SQLite reicht für einen ersten Blick, bricht aber bei mehreren gleichzeitigen Nutzern und blockiert spätere Migrationen.
Die folgende Aufteilung ist der Standardaufbau für produktive Installationen:
| Service | Image | Aufgabe |
|---|---|---|
app |
nextcloud:31-apache |
PHP-Anwendung inklusive Webserver |
db |
mariadb:11.4 |
Persistente Datenhaltung |
redis |
redis:7-alpine |
Transaktionales File-Locking und Cache |
cron |
nextcloud:31-apache |
Hintergrundjobs alle fünf Minuten |
proxy |
caddy:2-alpine |
HTTPS, Zertifikate, HSTS |
graph TD
A["Browser oder Desktop-Client"] -->|HTTPS 443| B["Caddy (proxy)"]
B -->|HTTP 80| C["nextcloud:31-apache (app)"]
C --> D["MariaDB 11.4 (db)"]
C --> E["Redis: Cache und File-Locking"]
F["cron-Container mit /cron.sh"] -->|alle 5 Minuten| D
F --> G["Volume nextcloud:/var/www/html"]
C --> G
Voraussetzungen
- Ein Linux-Host mit Docker Engine 24 oder neuer und dem Compose-Plugin v2 (
docker compose version), zum Beispiel eine skalierbare Cloud-VM mit 2 vCPU und 4 GB RAM - Ein DNS-A-Record für
<deine-domain>, der auf die öffentliche IP des Hosts zeigt - Die Ports 80 und 443 von außen erreichbar, sonst schlägt die ACME-Validierung von Caddy fehl
- Mindestens 20 GB freier Speicher für Datenbank und Nutzerdaten
Prüfe die Docker-Version, bevor du weitermachst:
$ docker compose version
Docker Compose version v2.29.7Projektverzeichnis und .env anlegen
Lege ein Projektverzeichnis unter /srv/nextcloud an und schreibe alle Zugangsdaten in eine .env-Datei mit den Rechten 600. Compose liest diese Datei automatisch, wenn sie neben der compose.yaml liegt.
$ sudo mkdir -p /srv/nextcloud
$ cd /srv/nextcloud
$ openssl rand -base64 24Erzeuge für jedes Passwort einen eigenen Wert und trage ihn ein:
# /srv/nextcloud/.env
NC_DOMAIN=cloud.deine-domain.de
MARIADB_ROOT_PASSWORD=<zufallswert-1>
MARIADB_PASSWORD=<zufallswert-2>
REDIS_PASSWORD=<zufallswert-3>
NC_ADMIN_USER=admin
NC_ADMIN_PASSWORD=<zufallswert-4>$ sudo chmod 600 /srv/nextcloud/.envPassende Infrastruktur bei centron
Zum Mitmachen braucht es keine eigene Hardware: ccloud³ VMs mit vollem Root-Zugriff, stundengenau abgerechnet und in Sekunden startklar. Cloud-Server mieten →
compose.yaml schreiben
Die compose.yaml definiert fünf Services und fünf benannte Volumes. Benannte Volumes statt Bind-Mounts sind hier die richtige Wahl, weil Nextcloud im Container als www-data mit UID 33 schreibt und Bind-Mounts auf dem Host regelmäßig zu Rechteproblemen führen.
# /srv/nextcloud/compose.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_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
MARIADB_DATABASE: nextcloud
MARIADB_USER: nextcloud
MARIADB_PASSWORD: ${MARIADB_PASSWORD}
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
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
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
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: ${NC_ADMIN_USER}
NEXTCLOUD_ADMIN_PASSWORD: ${NC_ADMIN_PASSWORD}
NEXTCLOUD_TRUSTED_DOMAINS: ${NC_DOMAIN}
OVERWRITEPROTOCOL: https
OVERWRITECLIURL: https://${NC_DOMAIN}
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:
condition: service_healthy
volumes:
- nextcloud:/var/www/html
proxy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
NC_DOMAIN: ${NC_DOMAIN}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- app
volumes:
db:
redis:
nextcloud:
caddy_data:
caddy_config:Drei Einstellungen sind erklärungsbedürftig:
--transaction-isolation=READ-COMMITTED: Nextcloud verlangt dieses Isolationslevel. Ohne die Einstellung meldet der Admin-Bereich eine Warnung, und es kommt bei paralleler Last zu Deadlocks.TRUSTED_PROXIES: ohne diesen Wert sieht Nextcloud die interne Container-IP von Caddy als Client-IP. Rate-Limiting und Brute-Force-Schutz greifen dann gegen den Proxy statt gegen den Angreifer.OVERWRITEPROTOCOL: https: Der App-Container spricht intern HTTP. Ohne den Overwrite generiert Nextcloudhttp://-Links, und Client-Verbindungen brechen ab.
Reverse Proxy mit Caddy konfigurieren
Caddy holt und erneuert das Let's-Encrypt-Zertifikat automatisch, sobald die Domain auf den Host zeigt. Die Konfiguration bleibt entsprechend kurz:
# /srv/nextcloud/Caddyfile
{$NC_DOMAIN} {
encode zstd gzip
header Strict-Transport-Security "max-age=31536000; includeSubDomains"
redir /.well-known/carddav /remote.php/dav 301
redir /.well-known/caldav /remote.php/dav 301
reverse_proxy app:80
}Die beiden redir-Zeilen brauchen CalDAV- und CardDAV-Clients für die Service-Discovery. Setze den HSTS-Header erst, wenn das Zertifikat steht: Browser merken sich den Wert ein Jahr lang.
Stack starten und Installation prüfen
Starte den Stack im Hintergrund und beobachte den ersten Durchlauf. Die Erstinstallation dauert je nach Host ein bis drei Minuten, weil der Entrypoint die Datenbank anlegt und die Standard-Apps aktiviert.
$ cd /srv/nextcloud
$ docker compose up -d
$ docker compose logs -f appSobald in den Logs Initializing finished erscheint, prüfst du den Status über die Kommandozeilen-Schnittstelle occ:
$ docker compose exec -u www-data app php occ status
- installed: true
- version: 31.0.5.1
- versionstring: 31.0.5
- edition:
- maintenance: false
- needsDbUpgrade: falseNextcloud legt bei der Installation nicht alle empfohlenen Datenbank-Indizes an. Hole das direkt nach:
$ docker compose exec -u www-data app php occ db:add-missing-indices
$ docker compose exec -u www-data app php occ maintenance:repair --include-expensiveRufe anschließend https://<deine-domain> auf. Du solltest ohne Zertifikatswarnung auf der Anmeldemaske landen und dich mit den Werten aus NC_ADMIN_USER und NC_ADMIN_PASSWORD einloggen können.
Was macht der Cron-Container?
Der Cron-Container führt alle fünf Minuten das Skript /cron.sh aus, das cron.php mit den Rechten von www-data startet und damit Hintergrundjobs wie Papierkorb-Bereinigung, Vorschaubilder, Volltextindex und Federation-Synchronisierung abarbeitet. Er teilt sich das Volume nextcloud mit dem App-Container und braucht deshalb kein eigenes Nextcloud-Verzeichnis.
Stelle nach dem ersten Start die Job-Ausführung in der Weboberfläche unter Verwaltungseinstellungen → Grundeinstellungen auf Cron um, oder setze den Wert direkt:
$ docker compose exec -u www-data app php occ background:cron
$ docker compose exec -u www-data app php occ config:app:get core lastcronBleibt lastcron über mehrere Minuten unverändert, läuft der Cron-Container nicht. Prüfe das mit docker compose ps cron.
Wie aktualisiert man Nextcloud im Container?
Ein Update erfolgt über den Image-Tag: Du trägst den neuen Major-Tag in der compose.yaml ein, ziehst das Image und startest den Stack neu, woraufhin der Entrypoint occ upgrade selbst ausführt. Überspringe dabei niemals eine Major-Version, Nextcloud unterstützt nur den Sprung um genau eine Hauptversion.
$ docker compose exec -u www-data app php occ maintenance:mode --on
$ docker compose pull app cron
$ docker compose up -d
$ docker compose exec -u www-data app php occ maintenance:mode --offHalte den Tag immer auf einer konkreten Hauptversion wie 31-apache. Der Tag latest springt beim nächsten Release ungefragt auf eine neue Hauptversion und kann Apps deaktivieren, die noch nicht kompatibel sind.
Backup von Datenbank und Volume
Ein Nextcloud-Backup besteht immer aus zwei Teilen: dem SQL-Dump und dem Verzeichnis /var/www/html mit Konfiguration, Apps und Nutzerdaten. Beide müssen denselben Stand haben, deshalb läuft die Sicherung im Wartungsmodus.
$ cd /srv/nextcloud
$ docker compose exec -u www-data app php occ maintenance:mode --on
$ docker compose exec db sh -c 'mariadb-dump -u root -p"$MARIADB_ROOT_PASSWORD" nextcloud' > /srv/backup/nextcloud-db.sql
$ docker run --rm -v nextcloud_nextcloud:/data:ro -v /srv/backup:/backup alpine tar czf /backup/nextcloud-html.tar.gz -C /data .
$ docker compose exec -u www-data app php occ maintenance:mode --offDer Volume-Name setzt sich aus dem Projektnamen und dem Volume-Schlüssel zusammen, hier also nextcloud_nextcloud. Prüfe den tatsächlichen Namen mit docker volume ls, wenn dein Verzeichnis anders heißt.
Troubleshooting
Access through untrusted domain: Die aufgerufene Domain steht nicht in trusted_domains. NEXTCLOUD_TRUSTED_DOMAINS wirkt nur bei der Erstinstallation, danach setzt du den Wert direkt:
$ docker compose exec -u www-data app php occ config:system:set trusted_domains 1 --value=cloud.deine-domain.deAlle Zugriffe kommen von derselben IP: TRUSTED_PROXIES passt nicht zum Docker-Netz. Ermittle das tatsächliche Subnetz und trage es ein:
$ docker network inspect nextcloud_default -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
172.18.0.0/16Caddy bekommt kein Zertifikat: In den Logs steht meist connection refused oder timeout. Prüfe, ob Port 80 offen ist und der DNS-Record aufgelöst wird. Läuft parallel ein nginx oder Apache auf dem Host, belegt dieser die Ports, und der Proxy-Container startet nicht.
Redis: Connection refused: Der Wert von REDIS_HOST_PASSWORD weicht vom --requirepass des Redis-Containers ab. Beide lesen aus REDIS_PASSWORD, also reicht ein docker compose up -d --force-recreate redis app nach einer Korrektur in der .env.
Fazit
Der Stack aus App, MariaDB, Redis, Cron und Caddy ist die kleinste Konfiguration, die man ohne Vorbehalte produktiv betreiben kann. Pinne die Image-Tags auf konkrete Hauptversionen, halte .env auf 600 und teste die Wiederherstellung des Backups mindestens einmal, bevor du echte Daten in die Instanz legst. Als nächster Schritt lohnt sich der Blick auf occ für Nutzerverwaltung und auf einen externen Object Storage als primären Speicher, sobald die Nutzerdaten über die lokale Festplatte hinauswachsen.
Weitere Docker-Themen
- Docker-Volumes mit Restic verschlüsselt sichern
- Ollama im Docker-Container betreiben
- Docker-Images mit mehrstufigen Builds verkleinern
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.