Zum Hauptinhalt springen
  1. Beiträge/
  2. Kleines Homelab/
  3. k3s-prod: Kubernetes Cluster Konfiguration und Anwendungsbereitstellung/

Nextcloud

··2311 Wörter· ·
Inhaltsverzeichnis

Nextcloud
#

Quelle
#

Die Anwendung wird als generisches app-template Helm-Chart von BJW-S Labs bereitgestellt. Das verwendete Docker-Image ist ghcr.io/linuxserver/nextcloud.

Komponenten
#

Diese Nextcloud-Instanz besteht aus mehreren Komponenten, die zusammenarbeiten:

  • Nextcloud: Die Hauptanwendung, die die Dateihosting- und Kollaborationsfunktionen bereitstellt.
  • MariaDB: Die relationale Datenbank, die zur Speicherung der Metadaten von Nextcloud verwendet wird.
  • Redis: Ein In-Memory-Datenspeicher, der für Caching und Dateisperren verwendet wird, um die Leistung zu verbessern.
  • ClamAV: Eine Antiviren-Engine, die hochgeladene Dateien auf Malware scannt.
  • PhpMyAdmin: Ein webbasiertes Verwaltungstool für die MariaDB-Datenbank, verfügbar unter /php.

Dokumentation
#

Funktion
#

Nextcloud ist die zentrale Plattform für das Datei-Hosting und die Zusammenarbeit. Sie besteht aus mehreren Diensten, die in einem Pod zusammengefasst sind:

  • Nextcloud: Die Hauptanwendung (PHP), die die Weboberfläche und die APIs bereitstellt.
  • MariaDB: Die Datenbank, in der alle Metadaten, Benutzerinformationen und Konfigurationen gespeichert werden.
  • Redis: Dient als schneller In-Memory-Cache und für das Transactional File Locking, um die Leistung zu verbessern.
  • ClamAV: Ein Virenscanner, der hochgeladene Dateien automatisch auf Malware überprüft.
  • phpMyAdmin: Eine Weboberfläche zur Verwaltung der MariaDB-Datenbank wird ebenfalls bereitgestellt.

Lokale Anpassungen
#

Die Konfiguration erfolgt über die values.yaml-Datei.

Wichtige Einstellungen
#

  • Ingress: Der Zugriff auf die Weboberfläche wird über einen Ingress mit einem Hostnamen ermöglicht.
  • Speicher: Alle persistenten Daten (Benutzerdateien, Konfiguration, Datenbank, Redis, ClamAV-Signaturen) liegen auf Longhorn-Volumes.
  • Virenscanner: Der integrierte ClamAV-Dienst ist so konfiguriert, dass er über einen Socket mit der Nextcloud-Anwendung kommuniziert.

php-fpm-Tuning
#

php/www2.conf wird als ConfigMap nach /config/php/www2.conf gemountet und überschreibt die Pool-Defaults des Images.

Hintergrund: Das Image liefert pm.max_children = 5 aus, und weder request_terminate_timeout noch pm.max_requests sind gesetzt. In dieser Kombination genügen fünf hängende Requests, um die Instanz bis zum manuellen Neustart lahmzulegen — jeder weitere Request läuft dann in den 60-Sekunden-Timeout von nginx und wird als 504 beantwortet, auch das leere /index.php/204. Am 20.08.2026 ist genau das passiert: 5/5 Worker auf 100 % CPU, über 18 Minuten CPU-Zeit auf einem einzelnen Request.

Zwei Dinge hängen zusammen und dürfen nicht getrennt geändert werden:

  • pm.max_children = 24 ist nur vertretbar, weil der Nextcloud-Container ein CPU-Limit hat. Ohne das Limit könnten 24 Amok-Worker sämtliche Kerne des Knotens samt Control Plane belegen. Wer den Wert anhebt, muss das CPU-Limit mit anheben — und der Knoten muss die Kerne haben.
  • request_terminate_timeout = 300 räumt hängende Requests ab, während request_slowlog_timeout = 5s vorher schon einen vollständigen PHP-Backtrace nach /config/log/php/www.slow.log schreibt. Diagnose und Stabilität schließen sich hier also nicht aus.

Beim Containerstart erscheint **** Permissions could not be set ... ****. Das ist erwartet: Der LSIO-Init ruft lsiown -R abc:abc /config/php und lsiown -R abc:abc /config/www/nextcloud auf und kann die read-only gemounteten Pfade nicht chownen. lsiown fängt den Fehler ab und liefert 0, der Init läuft normal weiter.

Nachtrag 24.08.2026 — CPU-Limit, Opcache, ptrace
#

Vier Tage später hing die Instanz erneut, diesmal in Schüben statt dauerhaft. Der Befund war ein anderer, und er hat drei Änderungen ausgelöst.

Das CPU-Limit von 4 war zu knapp. Gemessen an der cgroup:

1nr_periods      4330
2nr_throttled    2904        →  67 % aller Perioden hart gestoppt
3throttled_usec  714124122   →  714 s Zwangspause

Das Limit hat aus „ein Prozess frisst zu viel" ein „alle Requests dauern 10 s" gemacht; im Access-Log stand durchgehend HTTP 499, also Abbruch durch den Client. Es steht jetzt auf 8 — Schutz bleibt, Drosselung im Normalbetrieb entfällt.

Der Backtrace ließ sich nicht schreiben. php-fpm erkannte die langsamen Requests, scheiterte aber am Auslesen:

1ERROR: failed to ptrace(ATTACH) child 423: Operation not permitted (1)
2WARNING: child 423, script 'index.php' executing too slow (12.196865 sec)

Dem Container fehlte CAP_SYS_PTRACE. Dabei fiel auf, dass die drop-Liste in values.yaml nie gewirkt hat: CapBnd war exakt der containerd-Standardsatz 00000000a80425fb, MKNOD/NET_RAW/AUDIT_WRITE also noch gesetzt. Kubernetes erwartet die Namen ohne CAP_-Präfix. Beides ist korrigiert — die Drops greifen ab jetzt zum ersten Mal wirklich, was auch heißt: Wenn danach etwas fehlschlägt, das rohe Sockets oder mknod braucht, liegt es daran.

Der Opcache ist zu klein für den Bestand. 17059 PHP-Dateien (7529 Core, 9530 Apps) gegen max_accelerated_files = 10000, das intern auf 16229 Slots aufgerundet wird. Passt der Working Set nicht hinein, wird bei jedem Request nachkompiliert — und das ist User-CPU, was zur Messung passt: user_usec 1202 s gegen system_usec 14,8 s, bei nur 3 bis 40 Requests pro Minute. php/zz-zyria.ini hebt jetzt auf 24000 Dateien und 256 MB an.

Das ist ein begründeter Verdacht, kein Beweis. Der Laufzeit-Status des Opcache ließ sich nicht auslesen: Der nginx-Vhost leitet eigene PHP-Dateien nach index.php um, und Nextclouds eigene Opcache-Prüfung wird im CLI-Kontext übersprungen. Ausgeschlossen und getestet wurden dagegen DNS, Brute-Force-Drosselung, LDAP-Erreichbarkeit und die Datenbank.

Betroffen waren ausschließlich authentifizierte Endpunkte, dominiert von PROPFIND auf /remote.php/dav/principals/users/…. Unauthentifiziertes antwortete durchgehend in 10 bis 36 ms.

Aufloesung 25.08.2026 — es war DNS
#

Der php-fpm-Slowlog hat die Ursache der Schuebe festgenagelt:

1script_filename = /app/www/public/remote.php
2  ldap_start_tls()          user_ldap/lib/LDAP.php:304     <- hier stand er
3  startTls() -> doConnect() -> establishConnection()
4  bind() -> areCredentialsValid() -> checkPassword()
5  checkTokenCredentials()   lib/private/User/Session.php:732
6  validateToken() -> validateSession()

Nextcloud validiert Session-Tokens periodisch gegen LDAP nach. Dabei blieb ldap_start_tls() haengen, weil addc.zyria.de clusterweit auf die oeffentliche WAN-Adresse aufgeloest wurde, die per Hairpin-NAT nicht erreichbar ist. Jeder Verbindungsaufbau lief erst dort hinein und fiel dann auf IPv6 zurueck:

1vorher:  17 ms / 16 ms / 3077 ms   (derselbe Aufruf, Deckel bei 15 s)
2nachher: 8,5 bis 17,3 ms           (12 Messungen, Mittel 11,1 ms)

Das erklaert alle drei Auffaelligkeiten: nur authentifizierte Endpunkte (nur die fassen LDAP an), schubweises Auftreten (wenn mehrere Clients gleichzeitig synchronisieren und Tokens faellig werden) und Requests, die warten statt zu rechnen (cpu=0,42 % bei 7,1 s).

Behoben ausserhalb dieser App, in apps/nameserver und apps/coredns — der DC-Hostname liegt in der oeffentlichen Zone zyria.de und war deshalb durch alle intern weitergeleiteten Zonen gefallen. Betraf ebenso mailstack, self-service-password und wireguard.

Zusaetzlich als Sicherheitsnetz, per occ gesetzt (die LDAP-Konfiguration liegt in der Datenbank, nicht in values.yaml):

1occ ldap:set-config s01 ldapConnectionTimeout 3

Vorher 15. Sollte ein LDAP-Ziel je wieder unerreichbar werden, kostet das jetzt 3 statt 15 Sekunden pro Versuch. Fuer einen DC, der im LAN in 1 ms antwortet, sind 3 Sekunden reichlich Reserve.

Die Opcache-Vergroesserung weiter oben war nicht die Loesung. Sie beruhte auf der Fehldeutung einer Summenstatistik (user_usec gegen system_usec); die Werte pro Request zeigten das Gegenteil. Sie bleibt drin, weil sie an der Dateizahl gemessen sachlich richtig ist, erklaert die Haenger aber nicht.

Abgleich mit der Nextcloud-Doku
#

Gegen PHP-Konfiguration und Server-Tuning geprüft. Das LSIO-Image ist überwiegend bereits konform:

Einstellung Doku Ist
opcache.save_comments 1 (strikt gefordert) 1 Image
opcache.jit 1255 1255 → disable abgeschaltet, siehe unten
opcache.jit_buffer_size 8M 128M → 0 mit dem JIT abgeschaltet
opcache.revalidate_freq 60 60 zz-zyria.ini
output_buffering 0 0 Image
apc.enable_cli 1 1 Image
opcache.enable_cli 1 0 → 1 ergänzt
memory_limit min. 512M -1 → 1G korrigiert, siehe unten
request_slowlog_timeout 5s 10s → 5s angeglichen
pm.max_children 30 (Beispiel) 24 bewusst abweichend

pm.max_children bleibt bei 24 statt der 30 aus dem Doku-Beispiel, weil der Wert hier an das CPU-Limit von 8 gekoppelt ist und nicht frei gewählt werden kann.

Zum JIT: Seit dem 15.09.2026 ist opcache.jit in zz-zyria.ini abgeschaltet. Am 06.09. und 15.09. liefen php-fpm-Worker minutenlang mit einem vollen Kern, ohne ein Byte zu lesen oder zu schreiben, bis request_terminate_timeout sie nach 300 s beendete. nginx antwortete in der Zeit mit 504. Betroffen waren vor allem die PROPFIND-Abfragen des Desktop-Clients auf das Wurzelverzeichnis und /settings/admin/overview.

Der Slowlog zeigte in fast jedem Trace user_ldap Group_Proxy::setup(), aufgerufen beim Einbinden der Group Folders. Die Funktion tut nichts Rechenintensives, bei JIT-kompiliertem Code sind solche Backtraces aber unzuverlässig. LDAP war währenddessen erreichbar (occ ldap:test-config s01 in 0,35 s). Ohne JIT zeigt der Slowlog die tatsächliche Stelle, und bleiben die Hänger aus, war der JIT die Ursache. Für Nextcloud bringt der JIT kaum messbaren Nutzen.

Zum memory_limit: In der values.yaml stand PHP_MEMORY_LIMIT: "1G" als Env-Variable — wirkungslos. Das LSIO-Image wertet sie nirgends aus (kein Treffer in /etc/s6-overlay) und setzt memory_limit = -1, also unbegrenzt, fest in /etc/php84/conf.d/nextcloud.ini. Unbegrenzt ist bei 24 Workern riskant: Ein einzelner Ausreißer läuft bis ans Container-Limit von 8Gi und löst den OOM-Killer aus, der dann den ganzen Pod trifft. Mit 1G bricht stattdessen nur der eine Request ab. Die Variable wurde entfernt, der Wert steht jetzt in php/zz-zyria.ini.

Für apps/nextcloud-new gilt das nicht — das offizielle Image wertet PHP_MEMORY_LIMIT aus, dort ist die Env-Variable richtig.

Request-Protokollierung
#

access.log in php/www2.conf protokolliert jeden Request mit Dauer, CPU-Anteil und Peak-Memory nach /config/log/php/www.access.log.

Bewusst php-fpm und nicht nginx: Der nginx-Vhost ist eine LSIO-verwaltete Datei im PVC und läuft bei Image-Updates auseinander, und php-fpm liefert mehr Felder. Der Slowlog zeigt außerdem nur, was über 10 s liegt — hier ist auch der Request bei 3 oder 8 Sekunden sichtbar. Logrotate deckt /config/log/php/*.log bereits ab (weekly, rotate 7).

Caching und File-Locking
#

config/zz-zyria.config.php wird als ConfigMap nach /config/www/nextcloud/config/zz-zyria.config.php gemountet.

Schlüssel Wert Warum
memcache.local APCu prozesslokaler Speicherzugriff statt Netzwerk-Roundtrip
memcache.locking Redis muss prozessübergreifend sichtbar sein
memcache.distributed Redis unverändert

memcache.local und memcache.locking standen vertauscht: APCu war das Locking-Backend. APCu ist prozesslokal — Worker 3 sah die Sperren von Worker 7 nicht und der Cron-Prozess gar keine, Transactional File Locking war damit faktisch wirkungslos. Mit pm.max_children = 24 statt 5 wächst die Kollisionsfläche, der Fix wurde durch das Pool-Tuning also dringender.

Weil APCu jetzt den kompletten lokalen Cache hält statt nur ein paar Lock-Bytes, hebt php/zz-zyria.ini zusätzlich apc.shm_size von 32M auf 128M. Die beiden Änderungen gehören zusammen.

Vorsicht bei späteren Änderungen: Nextcloud lädt config.php zuerst und danach alle *.config.php in alphabetischer Reihenfolge. Die gemountete Datei gewinnt also immer. Wer einen dieser drei Schlüssel über die Admin-Oberfläche oder occ config:system:set ändert, bekommt eine Erfolgsmeldung — der Wert landet in config.php und wird beim nächsten Request wieder überschrieben. Deshalb steht in der gemounteten Datei bewusst nur dieses Minimum; alles andere gehört weiterhin nach config.php.

Trusted Proxies
#

Ebenfalls in config/zz-zyria.config.php. Nextcloud liest X-Forwarded-For nur, wenn die verbindende Adresse in dieser Liste steht. Sonst gilt die Proxy-Adresse selbst als Client, und Brute-Force-Schutz wie Rate-Limiting treffen den Ingress statt den Angreifer — ein einzelner auffälliger Client bremst dann alle anderen mit aus.

Eintrag Herkunft
10.0.0.0/8 Pod-Netz (IPv4). Breiter als nötig, siehe unten
fd21:da66:909b::/48 Pod-Netz (IPv6), neu ergänzt
172.16.0.0/12 aus früheren Stack-Generationen, sieht nach Docker-Bridge aus
2a01:4f8:212:350c::/64 Hetzner, Zweck ungeklärt
2a01:4f8:10a:893::/64 Hetzner, Zweck ungeklärt

Das v6-Präfix fehlte. Zum Zeitpunkt des Einbaus war der Service nextcloud-main noch SingleStack/IPv4, die Lücke also latent — im Nextcloud-Log standen aber 15 ältere Einträge mit fd21:da66:909b:2::cbb1 als vermeintlicher Client-Adresse, es war also schon einmal anders.

Der Service steht inzwischen auf RequireDualStack (siehe values.yaml), passend zu den übrigen Services im Cluster. Damit ist der Eintrag aktiv und notwendig: Ohne ihn würde jeder über IPv6 eingehende Request die Traefik-Pod-IP als Client protokollieren.

Zwei offene Punkte, bewusst nicht auf Verdacht geändert:

  • 10.0.0.0/8 ist breiter als nötig (Pods liegen in 10.44.0.0/16, Services in 10.45.0.0/16). Eingrenzen, sobald geklärt ist, ob im 10er-Netz noch etwas anderes davorhängt.
  • Ob die beiden Hetzner-Präfixe und 172.16.0.0/12 noch gebraucht werden, ist ungeklärt. Sie stammen aus der Zeit vor der Migration in den Cluster.

Arrays verhalten sich anders als Skalare. Bei einem Skalar gewinnt die gemountete Datei vollständig — bei memcache.local nachgewiesen. Bei einem Array werden die Werte dagegen pro Index vereinigt. Das ist beim Einbauen aufgefallen: config.php hatte die Indizes 0,1,2,4,5 und die gemountete Datei 0 bis 4, das Ergebnis waren sechs Einträge mit einer Dublette.

Deshalb wurde trusted_proxies per occ config:system:delete trusted_proxies vollständig aus config.php entfernt. Die gemountete Datei ist damit die einzige Quelle. Wer einen Proxy hinzufügt, muss das hier tun; ein occ config:system:set trusted_proxies … würde einen zusätzlichen Index anlegen statt einen bestehenden zu ersetzen.

Nebenwirkung, die man kennen muss: Fällt der ConfigMap-Mount weg, ist trusted_proxies leer. Nextcloud vertraut dann keinem Proxy mehr und hält die Traefik-Adresse für den Client — Brute-Force-Schutz und Rate-Limiting träfen dann alle Nutzer gemeinsam.

Collabora Online (Nextcloud Office)
#

Collabora kommt aus dem offiziellen Chart collabora-online von collaboraonline.github.io/online, eingebunden als zweiter helmCharts-Eintrag in kustomization.yaml. Die Werte stehen in values-collabora.yaml.

Collabora hat keine eigene Subdomain. Der Nextcloud-Ingress leitet die Pfade /browser, /hosting/discovery, /hosting/capabilities und /cool auf cloud.casa-due-pur.de an den Service collabora-online. Grund ist die globale Traefik-Middleware kube-system-default-headers: Sie setzt fuer alle Hosts frame-ancestors 'self'. Auf einer eigenen Subdomain liesse sich Collabora nicht in Nextcloud einbetten, ohne diese Regel fuer alle Anwendungen zu lockern.

In Nextcloud ist richdocuments so konfiguriert:

1occ config:app:set richdocuments wopi_url --value="https://cloud.casa-due-pur.de"
2occ config:app:set richdocuments disable_certificate_verification --value="no"
3occ config:app:set richdocuments wopi_allowlist --value="10.44.0.0/16,fd21:da66:909b::/48"
4occ richdocuments:activate-config

wopi_allowlist enthaelt die Pod-Netze, weil Collabora die WOPI-Aufrufe ueber Traefik schickt und Nextcloud per X-Forwarded-For die Pod-Adresse sieht. Der Wert * funktioniert nicht: IpUtils::checkIp behandelt ihn nicht als Platzhalter und lehnt jede Anfrage ab.

Die coolwsd-Optionen stehen als Container-Argumente im Patch in kustomization.yaml. Mit --use-env-vars, wie es das Image startet, ignoriert coolwsd die Variable extra_params.

Manuell angelegte Secrets
#

Zwei Secrets liegen bewusst nicht im Repo, solange es keine Secret-Verwaltung fuer ArgoCD gibt. Sie tragen die Annotation manage.casa-due-pur.de/origin und werden von ArgoCD nicht verwaltet.

Secret Schluessel Zweck
collabora-admin username, password vom Chart verlangt, die Admin-Konsole ist trotzdem abgeschaltet
collabora-proof-key proof_key, proof_key.pub WOPI-Proof-Key, mit dem Nextcloud Anfragen von Collabora pruefen kann

Neu anlegen oder rotieren, ohne dass Werte auf dem Terminal landen:

1T=$(mktemp -d) && chmod 700 "$T"
2ssh-keygen -q -t rsa -b 4096 -N "" -m PEM -C collabora-wopi-proof -f "$T/proof_key"
3printf admin > "$T/username"
4openssl rand -base64 48 | tr -d '\n/+=' | head -c 40 > "$T/password"
5kubectl -n nextcloud delete secret collabora-proof-key collabora-admin --ignore-not-found
6kubectl -n nextcloud create secret generic collabora-proof-key --from-file=proof_key="$T/proof_key" --from-file=proof_key.pub="$T/proof_key.pub"
7kubectl -n nextcloud create secret generic collabora-admin --from-file=username="$T/username" --from-file=password="$T/password"
8rm -rf "$T"
9kubectl -n nextcloud rollout restart deploy/collabora-online

Nach einem Wechsel des Proof-Keys in Nextcloud occ richdocuments:activate-config ausfuehren, damit die neue Discovery mit dem oeffentlichen Schluessel geladen wird.

Installation
#

Die Anwendung wird mittels Kustomize und Helm durch ArgoCD im Kubernetes-Cluster bereitgestellt. Die Konfiguration befindet sich im apps/nextcloud-Verzeichnis. Eine manuelle Installation kann mit folgendem Befehl durchgeführt werden:

1kubectl kustomize --enable-helm apps/nextcloud | kubectl apply -n nextcloud -f -

Abhängigkeiten
#

  • Ein laufender Kubernetes-Cluster.
  • Ein konfigurierter Ingress-Controller (z.B. Traefik).
  • Eine Zertifikatsmanagement-Lösung (z.B. cert-manager) zur Bereitstellung von TLS-Zertifikaten.
  • Longhorn als Storage-Provider für die PersistentVolumes.
Fabrice Kirchner
Autor
Fabrice Kirchner
stolzer Vater, Nerd, Admin