all systems self-hosted

~/selfhostjournal $

← Alle Artikel

Nextcloud in Docker hinter Traefik: meine Konfiguration erklärt

8–12 Minuten

Kurz gesagt

Problem: Ein Nextcloud-Container läuft in einer Minute. Damit er schnell ist, über HTTPS erreichbar, ohne Warnungen in der Admin-Übersicht und sicher zu sichern, braucht es deutlich mehr als docker run nextcloud.

Lösung: Ein Compose-Projekt mit Nextcloud, PostgreSQL, Redis und einem Cron-Container, veröffentlicht über Traefik, Anmeldung über Authentik, Dokumente im Browser mit dem eingebauten Collabora-Server und ein nächtliches Backup im Wartungsmodus.

Getestet mit: Nextcloud 35 (offizielles Image nextcloud:35, PHP 8.5), PostgreSQL 16, Redis, Traefik 3.7, Authentik, Docker Compose auf einem kleinen Heimserver

Nextcloud ist der Dienst in meinem Homelab, den die ganze Familie wirklich nutzt: Dateien, Kalender, Kontakte, Notizen und seit Kurzem auch Office-Dokumente. Deshalb ist es auch der Dienst, den ich am wenigsten kaputt machen möchte. Dieser Artikel geht meine Konfiguration durch: die Container, die Einstellungen, auf die es hinter einem Reverse Proxy ankommt, Single Sign-on, Nextcloud Office, die Pflege nach Updates und wie ich alles sichere. Er baut auf meinem Traefik-Artikel auf; wenn du noch keinen Reverse Proxy hast, fang dort an.

1. Der Stack im Überblick

ContainerImageAufgabe
nextcloudnextcloud:35Die Web-App (Apache + PHP)
postgrespostgres:16Die Datenbank. Deutlich besser als SQLite, sobald mehr als eine Person synchronisiert
redisredisDatei-Sperren und gemeinsamer Cache
nextcloud-cronnextcloud:35Führt alle 5 Minuten die Hintergrundjobs aus

Das Tag nextcloud:35 ist eine bewusste Entscheidung. Es folgt den Minor- und Patch-Versionen von Version 35, mein Update-Skript (es führt dreimal pro Woche docker compose pull && docker compose up -d aus) holt Sicherheitsupdates also von selbst. Eine neue Hauptversion kommt dagegen nie überraschend. Nextcloud kann immer nur eine Hauptversion auf einmal springen, und vor diesem Schritt möchte ich die Release Notes lesen und ein frisches Backup haben.

2. Die .env-Datei

Alles, was sich von Installation zu Installation unterscheidet, und jedes Passwort steht in einer .env-Datei neben der Compose-Datei. Mach sie nur für ihren Besitzer lesbar: chmod 600 .env.

NEXTCLOUD_IMAGE_TAG=nextcloud:35
NEXTCLOUD_HOSTNAME=cloud.example.com
NEXTCLOUD_URL=https://cloud.example.com
NEXTCLOUD_TIMEZONE=Europe/Berlin

NEXTCLOUD_DB_NAME=nextcloud
NEXTCLOUD_DB_USER=nextcloud
NEXTCLOUD_DB_PASSWORD=bitte-aendern-lang-und-zufaellig

NEXTCLOUD_REDIS_PASSWORD=bitte-auch-aendern

# wird nur beim allerersten Start gelesen
NEXTCLOUD_ADMIN_USERNAME=admin
NEXTCLOUD_ADMIN_PASSWORD=bitte-ebenfalls-aendern

Gute Passwörter erzeugst du zum Beispiel mit openssl rand -base64 32. Admin-Benutzer und -Passwort liest Nextcloud nur, wenn es sich beim ersten Start installiert. Sie später in der .env zu ändern, bewirkt nichts.

3. Datenbank, Redis und Cron

networks:
  nextcloud-network:
  proxy:
    external: true

volumes:
  nextcloud-app:
  nextcloud-postgres:
  redis-data:

services:
  postgres:
    image: postgres:16
    restart: unless-stopped
    volumes:
      - nextcloud-postgres:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: ${NEXTCLOUD_DB_NAME}
      POSTGRES_USER: ${NEXTCLOUD_DB_USER}
      POSTGRES_PASSWORD: ${NEXTCLOUD_DB_PASSWORD}
    networks:
      - nextcloud-network
    healthcheck:
      test: ["CMD", "pg_isready", "-q", "-d", "${NEXTCLOUD_DB_NAME}", "-U", "${NEXTCLOUD_DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 60s

  redis:
    image: redis
    restart: unless-stopped
    command: ["redis-server", "--requirepass", "${NEXTCLOUD_REDIS_PASSWORD}"]
    volumes:
      - redis-data:/data
    networks:
      - nextcloud-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

  nextcloud-cron:
    image: ${NEXTCLOUD_IMAGE_TAG}
    restart: unless-stopped
    entrypoint: /cron.sh
    volumes:
      - nextcloud-app:/var/www/html
      - /srv/nextcloud/data:/var/www/html/data
    networks:
      - nextcloud-network

Ein paar Details, die man leicht übersieht:

  • Datenbank und Redis hängen nur im nextcloud-network. Sie haben keine veröffentlichten Ports, und Traefik sieht sie nicht. Nur Nextcloud spricht mit ihnen.
  • Die Healthchecks sind hier wichtig. Nextcloud wartet auf service_healthy (siehe unten) und startet seine Installation oder sein Upgrade deshalb nie gegen eine Datenbank, die noch hochfährt.
  • Der Cron-Container nutzt dasselbe Image und dieselben Volumes wie Nextcloud, startet aber /cron.sh, das alle 5 Minuten cron.php aufruft. Stell Nextcloud nach dem ersten Start von AJAX auf Cron um: occ background:cron (oder Verwaltungseinstellungen → Grundeinstellungen).

4. Der Nextcloud-Container

  nextcloud:
    image: ${NEXTCLOUD_IMAGE_TAG}
    restart: unless-stopped
    volumes:
      - nextcloud-app:/var/www/html
      - /srv/nextcloud/data:/var/www/html/data
    environment:
      TZ: ${NEXTCLOUD_TIMEZONE}
      POSTGRES_HOST: postgres
      POSTGRES_DB: ${NEXTCLOUD_DB_NAME}
      POSTGRES_USER: ${NEXTCLOUD_DB_USER}
      POSTGRES_PASSWORD: ${NEXTCLOUD_DB_PASSWORD}
      REDIS_HOST: redis
      REDIS_HOST_PORT: 6379
      REDIS_HOST_PASSWORD: ${NEXTCLOUD_REDIS_PASSWORD}
      NEXTCLOUD_ADMIN_USER: ${NEXTCLOUD_ADMIN_USERNAME}
      NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_ADMIN_PASSWORD}
      NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_HOSTNAME}
      # hinter dem Reverse Proxy
      OVERWRITEHOST: ${NEXTCLOUD_HOSTNAME}
      OVERWRITEPROTOCOL: https
      OVERWRITECLIURL: ${NEXTCLOUD_URL}
      TRUSTED_PROXIES: 172.16.0.0/12
      # große Uploads und Office
      PHP_MEMORY_LIMIT: 8G
      PHP_UPLOAD_LIMIT: 8G
    networks:
      - nextcloud-network
      - proxy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80/"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 90s
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

Zwei Speicherorte. Das benannte Volume nextcloud-app enthält die Anwendung selbst, samt config/config.php und den Apps, die du aus dem App-Store installierst (custom_apps). Die Dateien der Benutzer liegen in einem Bind-Mount auf einer großen Platte (hier /srv/nextcloud/data). Durch die Trennung kann ich die Daten auf eine eigene SSD legen und beides unterschiedlich sichern. Vergiss nur das Volume nicht: config.php enthält das Secret und den Passwort-Salt der Instanz, und ohne sie wird eine Wiederherstellung nur aus den Daten deutlich schwieriger.

Die Reverse-Proxy-Einstellungen. Nextcloud bekommt von Traefik nur einfaches HTTP auf Port 80. Ohne diese vier Variablen baut es falsche Links, lehnt Anmeldungen über den „nicht vertrauenswürdigen“ Hostnamen ab oder zeigt jeden Besucher mit der IP-Adresse von Traefik:

VariableWozu
OVERWRITEHOSTDer Hostname, den Nextcloud in Links und Weiterleitungen verwendet
OVERWRITEPROTOCOL=httpsLinks beginnen mit https, obwohl die Anfrage innerhalb von Docker http ist. Ohne diese Variable laufen Anmelde-Weiterleitungen im Kreis oder brechen ab
OVERWRITECLIURLRichtige Links in E-Mails und in allem, was Hintergrundjobs oder occ erzeugen
TRUSTED_PROXIESNextcloud glaubt den X-Forwarded-For-Header nur von diesen Adressen. Dann zeigen Log, Brute-Force-Schutz und „Letzte Anmeldung“ die echte IP des Clients

172.16.0.0/12 deckt die Standard-Netzbereiche von Docker ab, in denen Traefik läuft. Trag ein, was docker network inspect proxy auf deinem Host anzeigt, und nicht mehr: Jeder Bereich, dem du vertraust, kann Client-IPs fälschen.

5. Veröffentlichen über Traefik

    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.nextcloud.rule=Host(`cloud.example.com`)"
      - "traefik.http.routers.nextcloud.entrypoints=websecure"
      - "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
      - "traefik.http.routers.nextcloud.middlewares=nextcloud-dav,nextcloud-wellknown,nextcloud-compress,nextcloud-hsts"
      - "traefik.http.services.nextcloud.loadbalancer.server.port=80"

      # CalDAV/CardDAV-Erkennung für Handys und Kalender-Apps
      - "traefik.http.middlewares.nextcloud-dav.redirectregex.permanent=true"
      - "traefik.http.middlewares.nextcloud-dav.redirectregex.regex=https?://([^/]*)/.well-known/(card|cal)dav"
      - "traefik.http.middlewares.nextcloud-dav.redirectregex.replacement=https://$${1}/remote.php/dav/"

      # alle anderen .well-known-URLs (webfinger, nodeinfo, ...)
      - "traefik.http.middlewares.nextcloud-wellknown.redirectregex.permanent=true"
      - "traefik.http.middlewares.nextcloud-wellknown.redirectregex.regex=https?://([^/]*)(/.well-known[^#]*)"
      - "traefik.http.middlewares.nextcloud-wellknown.redirectregex.replacement=https://$${1}/index.php$${2}"

      - "traefik.http.middlewares.nextcloud-compress.compress=true"
      - "traefik.http.middlewares.nextcloud-hsts.headers.stsSeconds=15552000"

Die beiden Weiterleitungen beheben die häufigste Warnung in der Admin-Übersicht („Dein Webserver ist nicht richtig eingerichtet, um /.well-known/caldav aufzulösen“). Außerdem finden Handys damit Kalender und Kontakte, wenn du nur den Hostnamen eingibst. Das $$ ist nötig, weil Compose ${1} sonst als Variable liest.

HSTS (die Zeile mit stsSeconds) sagt Browsern, dass sie diesen Host die nächsten 180 Tage nur noch über HTTPS aufrufen sollen. Das beseitigt eine weitere Warnung. Bei mir setzt eine allgemeine Security-Header-Middleware am Entrypoint websecure HSTS ohnehin für jeden Dienst (siehe den Artikel zum Absichern). Die Zeile bleibt trotzdem hier, damit der Nextcloud-Stack HSTS auch dann bekommt, wenn ich daran einmal etwas ändere.

Achtung, nur eine Middlewares-Zeile pro Router: Setzt eine Compose-Datei traefik.http.routers.nextcloud.middlewares zweimal, gibt es keinen Fehler. Die zweite Zeile ersetzt die erste stillschweigend. Genau das stand lange in meiner eigenen Datei: Eine frühere Zeile mit einer Buffering-Middleware war nie aktiv, und das ist mir erst beim Schreiben dieses Artikels aufgefallen (docker inspect zeigt die Labels, die der Container wirklich hat). Schreib alle Middlewares kommagetrennt in eine Zeile.

Für große Uploads brauchst du übrigens keine Buffering-Middleware. Traefik hat standardmäßig keine Größenbegrenzung für Anfragen, und die Nextcloud-Clients laden große Dateien ohnehin in Stücken hoch.

6. Anmeldung mit Authentik

Wie die meisten meiner Dienste nutzt Nextcloud Single Sign-on mit Authentik. Ich verwende die offizielle App OpenID Connect user backend (user_oidc). Die Einrichtung auf der Authentik-Seite (ein OAuth2/OpenID-Provider plus eine Anwendung) ist dieselbe wie in meinem Artikel zu WordPress-SSO. Die Redirect-URI für Nextcloud ist https://cloud.example.com/apps/user_oidc/code.

Danach im Nextcloud-Container:

alias occ='docker compose exec -u www-data nextcloud php occ'

occ app:install user_oidc

occ user_oidc:provider Authentik \
  --clientid="<Client-ID aus Authentik>" \
  --clientsecret="<Client-Secret aus Authentik>" \
  --discoveryuri="https://id.example.com/application/o/nextcloud/.well-known/openid-configuration" \
  --scope="openid email profile" \
  --mapping-uid=preferred_username \
  --mapping-display-name=name \
  --mapping-email=email \
  --unique-uid=0

Entscheidend sind die letzten beiden Optionen. --mapping-uid=preferred_username zusammen mit --unique-uid=0 macht den Nextcloud-Benutzernamen gleich dem Authentik-Benutzernamen statt eines langen Hashes. Ein Authentik-Benutzer anna landet so im bestehenden Nextcloud-Konto anna mit all ihren Dateien, und die Pfade bleiben lesbar.

Die Anmeldeseite zeigt jetzt einen Button „Log in with Authentik“. Die normale Nextcloud-Anmeldung bleibt darunter verfügbar. Die behalte ich mit Absicht: Wenn Authentik ausfällt, funktioniert das lokale Admin-Konto trotzdem. Desktop- und Handy-Clients melden sich über den Browser an und nutzen damit ebenfalls SSO.

Tipp: Setz die Forward-Auth-Middleware von Authentik nicht vor Nextcloud. Sie bricht die Sync-Clients und WebDAV, die sich nicht bei jeder Anfrage über den Browser anmelden können. Nimm stattdessen die OIDC-App.

7. Nextcloud Office ohne zusätzlichen Container

Um .docx-, .xlsx- und .odt-Dateien im Browser zu öffnen, nutze ich Nextcloud Office mit dem eingebauten CODE-Server (Collabora Online Development Edition). Er läuft im Nextcloud-Container: kein zusätzlicher Container, kein zusätzlicher Hostname, kein zusätzliches Zertifikat. Für einen Haushalt mit einer Handvoll Benutzer reicht das völlig.

occ app:install richdocumentscode   # der eingebaute Collabora-Server (auf ARM: richdocumentscode_arm64)
occ app:install richdocuments       # die App Nextcloud Office
occ richdocuments:activate-config

Danach sollte unter Verwaltungseinstellungen → Office stehen, dass der eingebaute Server verwendet wird. Die WOPI-URL zeigt auf https://cloud.example.com/custom_apps/richdocumentscode/proxy.php?req=.

Achtung, Security-Header: Der Editor läuft in einem iframe innerhalb von Nextcloud. Ein globaler Header X-Frame-Options: DENY oder SAMEORIGIN vom Reverse Proxy macht daraus ein leeres, graues Dokument. Nextcloud setzt seine eigenen Frame-Header, überlass diesen Header also der App.

Prüf auch, ob die Version der Office-App zu deiner Nextcloud-Hauptversion passt. Nach einem Upgrade bleibt manchmal eine ältere, nicht kompatible Version installiert; occ app:update --all behebt das.

8. Saubere Admin-Übersicht nach einem Update

Nach einem Hauptversions-Upgrade zeigt Verwaltungseinstellungen → Übersicht meistens ein paar neue Warnungen. Diese Befehle habe ich gebraucht, ungefähr in dieser Reihenfolge (mit frischem Backup):

# neue Datenbank-Indizes, die mit dem Upgrade kamen (dauert Sekunden bis Minuten)
occ db:add-missing-indices

# nur wenn die Übersicht es vorschlägt; kann bei großen Instanzen dauern
occ maintenance:repair --include-expensive

# Telefonnummern in Profilen ohne Ländervorwahl
occ config:system:set default_phone_region --value="DE"

# aufwendige Hintergrundjobs nachts ausführen (Stunde in UTC, 1 = 01:00-05:00)
occ config:system:set maintenance_window_start --type=integer --value=1

# dieselben Prüfungen wie die Admin-Übersicht, auf der Kommandozeile
occ setupchecks

Den Speicher-Cache richtet das offizielle Image selbst ein: APCu für den lokalen Cache und Redis für die Datei-Sperren, sobald REDIS_HOST gesetzt ist. Prüfen kannst du das mit occ config:system:get memcache.locking.

9. Backups

Nextcloud wird jede Nacht von einem Skript auf dem Host gesichert, nicht innerhalb des Stacks. Die Reihenfolge ist wichtig:

  1. occ maintenance:mode --on, damit sich während der Kopie keine Datei und keine Datenbankzeile ändert
  2. pg_dump der Datenbank in eine einfache SQL-Datei (nie die laufenden Datenbankdateien kopieren)
  3. rsync des Datenordners in einen Snapshot mit Datum, mit Bandbreitenlimit, damit die Platten nutzbar bleiben
  4. eine Kopie von config/, custom_apps/ und themes/ aus dem App-Volume, dazu die Compose-Datei und die .env
  5. occ maintenance:mode --off, in einem trap, damit das auch passiert, wenn ein Schritt fehlschlägt

In einer normalen Nacht dauert der Wartungsmodus ein paar Minuten. Unveränderte Dateien sind zwischen den Snapshots per Hardlink verknüpft, 14 Tage Verlauf kosten also kaum mehr Platz als eine Kopie. Wie Snapshots, Datenbank-Dumps und die Offline-Kopie zusammenspielen, steht in meinem Backup-Artikel.

Tipp: Führ nach einer Wiederherstellung occ maintenance:data-fingerprint aus. Damit erfahren die Sync-Clients, dass der Server in der Zeit zurückgesprungen ist. Sie fragen dann nach, bevor sie neuere Dateien überschreiben, statt still in irgendeine Richtung zu synchronisieren.

10. Wenn etwas nicht funktioniert

SymptomHäufige Ursache
„Zugriff über eine nicht vertrauenswürdige Domain“Der Hostname fehlt in trusted_domains. NEXTCLOUD_TRUSTED_DOMAINS wirkt nur bei der ersten Installation; später occ config:system:set trusted_domains 1 --value=cloud.example.com
Die Anmeldung landet wieder auf der Anmeldeseite, oder Links beginnen mit http://OVERWRITEPROTOCOL=https fehlt
Jeder Log-Eintrag zeigt dieselbe interne IPTRUSTED_PROXIES enthält das Netz von Traefik nicht
„Dein Webserver ist nicht richtig eingerichtet, um /.well-known/caldav aufzulösen“Die Weiterleitungs-Middlewares sind nicht aktiv. Prüf mit docker inspect, welche Middlewares der Router wirklich bekommen hat
Office-Dokumente öffnen sich als leere graue SeiteEin X-Frame-Options-Header vom Proxy oder eine Office-App-Version, die nicht zur Nextcloud-Version passt
„Letzte Hintergrundjob-Ausführung lief vor … Stunden“Der Cron-Container läuft nicht, oder Nextcloud steht noch auf AJAX

Die nützlichsten Befehle für den Rest:

# die neuesten Fehler aus dem Nextcloud-Log
occ log:tail 20

# worüber sich die Admin-Übersicht beschweren würde
occ setupchecks

# welche Labels (und damit welche Traefik-Middlewares) der Container wirklich hat
docker inspect nextcloud-nextcloud-1 --format '{{json .Config.Labels}}'

Was ich gelernt habe

  • Die Hauptversion festlegen. Mit nextcloud:35 bekomme ich Sicherheitsupdates automatisch, und das große Upgrade passiert, wenn ich es entscheide, mit Backup.
  • Die Reverse-Proxy-Einstellungen sind nicht optional. Die meisten seltsamen Nextcloud-Probleme hinter Traefik lagen an OVERWRITEPROTOCOL oder TRUSTED_PROXIES.
  • Prüfen, was wirklich läuft. Ein Label, das von einem späteren überschrieben wird, erzeugt keinen Fehler. docker inspect zeigt die Wahrheit, die Compose-Datei nur die Absicht.
  • Das App-Volume gehört ins Backup. Lange habe ich nur den Datenordner und die Datenbank gesichert. Ohne config.php ist eine Wiederherstellung deutlich schwieriger.
  • SSO über OIDC, nicht über Forward-Auth. Die Sync-Clients funktionieren weiter, und die lokale Anmeldung ist mein Notausgang.

Alles hier hat bei mir zum Zeitpunkt des Schreibens funktioniert. Software ändert sich, und dein Setup ist anders: mach vorher ein Backup, teste vorsichtig und wende die Schritte auf eigenes Risiko an. Details im Impressum.