Tutorials  /  Docker

Nextcloud mit Docker Compose betreiben

LLudwig · August 2026 ·8 Min. Lesezeit ·Docker, Tutorial

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:

Konsole
$ docker compose version
Docker Compose version v2.29.7

Projektverzeichnis 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.

Konsole
$ sudo mkdir -p /srv/nextcloud
$ cd /srv/nextcloud
$ openssl rand -base64 24

Erzeuge für jedes Passwort einen eigenen Wert und trage ihn ein:

bash
# /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>
Konsole
$ sudo chmod 600 /srv/nextcloud/.env
VM

Passende 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.

yaml
# /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 Nextcloud http://-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:

Caddyfile
# /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.

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

Sobald in den Logs Initializing finished erscheint, prüfst du den Status über die Kommandozeilen-Schnittstelle occ:

Konsole
$ 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: false

Nextcloud legt bei der Installation nicht alle empfohlenen Datenbank-Indizes an. Hole das direkt nach:

Konsole
$ 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-expensive

Rufe 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:

Konsole
$ docker compose exec -u www-data app php occ background:cron
$ docker compose exec -u www-data app php occ config:app:get core lastcron

Bleibt 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.

Konsole
$ 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 --off

Halte 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.

Konsole
$ 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 --off

Der 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:

Konsole
$ docker compose exec -u www-data app php occ config:system:set trusted_domains 1 --value=cloud.deine-domain.de

Alle Zugriffe kommen von derselben IP: TRUSTED_PROXIES passt nicht zum Docker-Netz. Ermittle das tatsächliche Subnetz und trage es ein:

Konsole
$ docker network inspect nextcloud_default -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
172.18.0.0/16

Caddy 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

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?

Our team will help you with your specific setup - in German or English, by people who run the platform themselves.

War dieses Tutorial hilfreich?

Your answer is stored anonymously and helps us improve our tutorials.

Kommentare

No comments yet - be the first to ask a question about this tutorial.

Sign in to comment

Comments are open to centron customers. Sign in to your account to ask a question about this tutorial.

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