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 #
- Nextcloud Documentation
- MariaDB Documentation
- Redis Documentation
- ClamAV Documentation
- PhpMyAdmin Documentation
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 = 24ist 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 = 300räumt hängende Requests ab, währendrequest_slowlog_timeout = 5svorher schon einen vollständigen PHP-Backtrace nach/config/log/php/www.slow.logschreibt. 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 ZwangspauseDas 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.phpum, 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 3Vorher 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_usecgegensystem_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/8ist breiter als nötig (Pods liegen in10.44.0.0/16, Services in10.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/12noch 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_proxiesleer. 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-configwopi_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-onlineNach 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.