Nextcloud – Betriebs­dokumentation

Nextcloud 33 auf K3s (AlmaLinux 9) · Letzte Aktualisierung: Mai 2026

Überblick

Nextcloud läuft auf einem Single-Node-K3s-Cluster auf AlmaLinux 9. Die gesamte Infrastruktur wird mit einem Ansible-Playbook (nextcloud-k3s.yml) aufgebaut und verwaltet. Das Playbook ist idempotent – es kann jederzeit neu ausgeführt werden, ohne Daten zu verändern oder Dienste unnötig zu unterbrechen.

Nextcloud
33.x (nextcloud:33-fpm)
Betriebssystem
AlmaLinux 9
Kubernetes
K3s (Single-Node)
Datenbank
MariaDB 10.11 (Host)
Cache / Locking
Redis (Host)
Online-Office
Collabora CODE 25.04
Design-Entscheidung: Single-Node Der Server läuft als einzelner Kubernetes-Node. Es gibt keine Hochverfügbarkeit. Updates und Neustarts verursachen eine kurze Ausfallzeit (typisch: 30–60 Sekunden). Die Recreate-Strategie im Deployment stellt sicher, dass immer nur ein Pod auf die HostPath-Volumes zugreift.

Architektur

Gesamtüberblick

Internet / Browser │ │ HTTPS :443 / HTTP :80 ▼ ┌─────────────────────────────────────────────────────┐ │ nginx-ingress F5 (K3s DaemonSet, hostNetwork=true) │ │ TLS-Terminierung via cert-manager (Let's Encrypt) │ │ Rate-Limiting: nc_login 5r/s (HTTP-Ebene) │ └───────────────┬─────────────────────┬───────────────┘ │ HTTP │ HTTP nextcloud.* │ collab. │ (WebSocket) ▼ ▼ ┌──────────────────────┐ ┌──────────────────���───┐ │ Pod: nextcloud │ │ Pod: collabora │ │ ┌────────────────┐ │ │ Collabora CODE │ │ │ nginx sidecar │ │ │ 25.04 │ │ │ :80 (static + │ │ │ :9980 │ │ │ PHP proxy) │ │ └──────────────────────┘ │ └───────┬────────┘ │ │ fastcgi │ :9000 │ │ ┌───────▼────────┐ │ │ │ nextcloud-fpm │ │ │ │ PHP 8.4 + FPM │ │ │ └────────────────┘ │ └──────────────────────┘ │ │ │ TCP :3306 │ TCP :6379 ▼ ▼ ┌──────────────┐ ┌───────────┐ │ MariaDB │ │ Redis │ │ 10.11 Host │ │ 7.x Host │ └──────────────┘ └───────────┘┌───────────────────────────────────────────┐ │ HostPath Volumes (auf /dev/vda4, 119 GB) │ │ /srv/nextcloud-www → /var/www/html │ │ /data → /var/www/html/data │ └───────────────────────────────────────────┘

Warum dieser Ansatz?

KomponenteWo läuft es?Begründung
Nextcloud PHP-FPM + nginx K3s Pod Container Einfache Updates durch Image-Wechsel; kein php-fpm auf dem Host
Collabora CODE K3s Pod Container Isoliert, eigene TLS-Terminierung durch nginx-ingress (F5)
MariaDB Host-Dienst Host Datenbankdaten liegen direkt auf dem Dateisystem; kein Container-Volume-Layer
Redis Host-Dienst Host Einfach, keine Persistenz nötig; bindet an Host-IP für Pod-Zugriff
nginx-ingress (F5) K3s DaemonSet hostNetwork Bindet direkt auf Port 80/443 des Hosts; kein externer Load Balancer nötig
cert-manager K3s Container Automatische Let's-Encrypt-Zertifikate; ersetzt certbot

Nextcloud-Pod im Detail

Beide Container teilen sich denselben Pod (= dasselbe Netzwerk-Namespace und die gleichen Volumes). Dadurch kann nginx via 127.0.0.1:9000 mit PHP-FPM kommunizieren, ohne einen Kubernetes-Service zu benötigen.

Init-Container: init-dirs

Läuft einmalig vor dem Start der Hauptcontainer. Setzt die Ownership der HostPath-Verzeichnisse auf UID 33 (www-data im Container) und setzt die Berechtigungen. Ohne diesen Schritt kann Nextcloud nichts in /var/www/html schreiben.

Container 1: nextcloud-fpm

Das offizielle nextcloud:33-fpm-Image. Führt PHP-FPM auf Port 9000 aus. Bei einem leeren /var/www/html installiert der Docker-Entrypoint Nextcloud automatisch anhand der Umgebungsvariablen (DB, Admin, Redis). Bei einem Update läuft occ upgrade automatisch.

Container 2: nginx

nginx-Sidecar (nginx:1.30-alpine) auf Port 80. Beantwortet statische Anfragen direkt aus dem gemeinsamen Volume; PHP-Anfragen werden per FastCGI an Port 9000 weitergeleitet. TLS ist bereits durch nginx-ingress (F5) terminiert – nginx spricht nur plain HTTP.

Speicher & Daten

Wichtig: Alle Verzeichnisse liegen auf derselben Partition /dev/vda4 (119 GB). Es gibt keine getrennten Volumes für App und Daten.
Pfad auf dem HostMountpoint im ContainerInhaltBesitzer
/srv/nextcloud-www /var/www/html PHP-App, Apps, Themes, config/ root:root 755 (Verz.)
33:33 für config/
/data /var/www/html/data Benutzerdateien, nextcloud.log 33:33 (www-data im Container)
/var/lib/mysql MariaDB-Datenbank (läuft direkt auf dem Host) mysql:mysql

Warum sind App und Daten getrennt?

Nextcloud-App-Dateien (/srv/nextcloud-www) und Benutzerdaten (/data) sind bewusst in zwei separate HostPath-Volumes aufgeteilt, weil:

Wichtige Konfigurationsdatei: config.php

Nextcloud liest alle *.php-Dateien aus /var/www/html/config/ automatisch. Es gibt zwei Quellen:

Ownership-Falle beim ConfigMap-Mount Kubernetes erstellt beim Mount eines ConfigMap-subPath das Elternverzeichnis ggf. als root:root. Deshalb muss /srv/nextcloud-www/config/ vorab mit owner: 33 angelegt werden (Ansible-Task next_k3s_deploy/tasks/dirs.yml).

Netzwerk & TLS

IP-Adressbereiche

BereichZweckRelevant für
10.42.0.0/16 K3s Pod-CIDR (Flannel) iptables-Regeln, trusted_proxies, WOPI-Allowlist, MariaDB-User-Grant
10.43.0.0/16 K3s Service-CIDR ClusterIP-Adressen der K8s Services
82.165.165.230 Öffentliche Host-IP (Testserver) DNS, WOPI-Allowlist, ExternalService-Endpoints

Benötigte DNS-Einträge

Vor dem ersten Playbook-Lauf müssen die DNS-Einträge beim Domain-Registrar angelegt sein. cert-manager benötigt sie für den Let's Encrypt HTTP-01-Challenge – ohne gültige Einträge schlägt die Zertifikatsausstellung fehl.

Name (Hostname)TypWertZweck
nextcloud.example.de A Öffentliche IP des Servers Nextcloud-Weboberfläche, Let's Encrypt TLS
collabora.example.de A Öffentliche IP des Servers Collabora CODE Online (Office-Bearbeitung), eigenes TLS-Zertifikat
Collabora braucht einen eigenen Hostnamen Collabora läuft als separater Ingress auf einem eigenen Subdomain-Namen. Wird nur ein A-Record angelegt (ohne Collabora), kann der WOPI-Callback von Nextcloud zu Collabora nicht funktionieren und Dokumente lassen sich nicht online bearbeiten.
IPv6 (optional) Falls der Server eine öffentliche IPv6-Adresse hat, können zusätzlich AAAA-Records für beide Hostnamen angelegt werden. Die Firewall-Regeln (fw_ipv6.sh) sind bereits konfiguriert.

Wie Pods den Host erreichen (MariaDB / Redis)

MariaDB und Redis laufen als Host-Dienste. Pods verbinden sich darüber via K8s ExternalServices: ein Service ohne Selector mit manuell gepflegten Endpoints, die auf die öffentliche IP des Hosts zeigen.

Pod → mariadb.nextcloud.svc.cluster.local (10.43.x.x)
   → Endpoint: 82.165.165.230:3306
   → iptables ACCEPT für 10.42.0.0/16 auf :3306

Der MariaDB-Datenbanknutzer ist auf den Pod-CIDR eingeschränkt ('nextcloud'@'10.42.%'). Eine direkte Verbindung vom Host funktioniert nur als root via Unix-Socket.

TLS und Proxy-Kette

Internet
  → nginx-ingress (F5) (Port 443, TLS terminiert, setzt X-Forwarded-For / -Proto)
    → nginx-Sidecar (Port 80, plain HTTP)
      → PHP-FPM (Port 9000, FastCGI)
        fastcgi_param HTTPS on
        fastcgi_param HTTP_SCHEME https

Nextcloud muss wissen, dass Anfragen über einen Proxy kommen:

'trusted_proxies'       => ['10.42.0.0/16'],
'forwarded_for_headers' => ['HTTP_X_FORWARDED_FOR'],
'overwriteprotocol'     => 'https',
Wichtig: trusted_proxies muss den Pod-CIDR enthalten Fehlt dieser Eintrag, zeigt Nextcloud im Admin-Panel die Warnung "Konfiguration des Reverse-Proxy-Headers ist falsch". nginx-ingress (F5) leitet Anfragen mit seiner Pod-IP (10.42.x.x) weiter – diese muss als vertrauenswürdiger Proxy eingetragen sein.

WOPI (Collabora)

Collabora ruft für die Dokument-Bearbeitung Nextclouds WOPI-API auf. Diese Rückrufe kommen von der Pod-IP des Collabora-Pods (10.42.x.x) – nicht von der Service-IP. Die wopi_allowlist muss daher den Pod-CIDR enthalten:

# inventory/host_vars/<host>/vars.yml
wopi_allowed_hosts:
  - "10.42.0.0/16"   # Pod-CIDR (Collabora-Pod → Nextcloud WOPI)
  - "10.43.0.0/16"   # Service-CIDR
  - "collabora.inspired-as-code.de"

Wichtige Dateien im Repository

Inventory & Variablen

DateiInhalt
inventory/group_vars/all.yml Globale Einstellungen: K3s-CIDRs, Container-Image-Tags, Helm-Chart-Versionen, Monitoring-Versionen
inventory/host_vars/<host>/vars.yml Hostspezifisch: Hostnames, IPs, DB-Name, Pfade, WOPI-Allowlist
inventory/host_vars/<host>/vault.yml Verschlüsselt! Passwörter: DB-Root, DB-User, Admin, Redis, SMTP, Grafana. Vor git-Commit verschlüsseln: ansible-vault encrypt inventory/host_vars/test/vault.yml

Ansible-Rollen (Nextcloud-spezifisch)

RolleAufgabe
next_packagesRepositories (MariaDB 10.11), Pakete (MariaDB, Redis, SELinux-Tools)
next_mariadbMariaDB-Setup, Datenbank und Benutzer anlegen, bind-address auf 0.0.0.0
next_redisRedis-Bind-Konfiguration (localhost + Host-IP)
next_selinuxSELinux enforcing, container_file_t für HostPath-Verzeichnisse
next_k3s_deployK8s-Manifeste: Namespace, Secrets, ConfigMaps, Deployments, Ingress, CronJob
next_configPost-Deploy via kubectl exec occ: trusted_domains, Apps, WOPI-Config

Templates (erzeugen die K8s-Manifeste)

TemplateErzeugtWichtig weil
nextcloud-deployment.yml.j2 Deployment + Service Image-Tags, Volumes, Readiness-Probes, Env-Variablen
custom-config-cm.yml.j2 ConfigMap nextcloud-custom-config Nextcloud-PHP-Konfiguration: Redis, Proxy, Locale, Session
nginx-cm.yml.j2 ConfigMap nextcloud-nginx-config nginx-Routing, .well-known-Redirects, MIME-Typen, PHP-FastCGI
secret.yml.j2 Secret nextcloud-secrets DB-Credentials, Admin-Passwort, SMTP-Daten – in den Pod als Env-Variablen
external-services.yml.j2 Services + Endpoints für MariaDB, Redis, Grafana Verbindung vom Pod zum Host-Dienst
collabora-deployment.yml.j2 Collabora Deployment + Service aliasgroup1-Env begrenzt erlaubte Nextcloud-Instanzen
cronjob.yml.j2 K8s CronJob Führt occ cron.php alle 5 Minuten aus

Variablen & Secrets

Wo Credentials liegen

Alle Passwörter stehen in inventory/host_vars/<host>/vault.yml, verschlüsselt mit Ansible Vault. Diese Datei darf niemals unverschlüsselt in git committed werden.

# Verschlüsseln vor dem Commit:
ansible-vault encrypt inventory/host_vars/test/vault.yml

# Entschlüsseln zum Bearbeiten:
ansible-vault decrypt inventory/host_vars/test/vault.yml
# (danach sofort wieder verschlüsseln!)

# Direkt im Editor öffnen (bevorzugt):
ansible-vault edit inventory/host_vars/test/vault.yml

Passwort-Eintrag für unbeaufsichtigte Runs

# .vault_pass (niemals committen – steht in .gitignore)
echo "mein-vault-passwort" > .vault_pass
# In ansible.cfg einkommentieren:
# vault_password_file = .vault_pass

Secrets im K8s-Cluster

Die Vault-Variablen fließen beim Playbook-Run in das K8s-Secret nextcloud-secrets im Namespace nextcloud. Das Secret wird als Umgebungsvariablen in den Pod injiziert.

# Secret anzeigen (dekodiert):
k3s kubectl get secret nextcloud-secrets -n nextcloud \
  -o jsonpath='{.data}' | python3 -c \
  "import sys,json,base64; [print(k,base64.b64decode(v).decode()) \
   for k,v in json.load(sys.stdin).items()]"

Container-Images & Versionen

Alle Image-Tags sind in inventory/group_vars/all.yml zentral definiert. Da imagePullPolicy standardmäßig IfNotPresent für getaggte Images ist, zieht K3s automatisch ein neues Image nur dann, wenn der Tag im Manifest geändert wurde.

# inventory/group_vars/all.yml
nextcloud_image_fpm:       "nextcloud:33-fpm"      # Nextcloud PHP-FPM
nextcloud_image_nginx:     "nginx:1.30-alpine"          # nginx Sidecar
nextcloud_image_collabora: "collabora/code:25.04.9.4.1" # Collabora CODE
nextcloud_image_busybox:   "busybox:1"             # Init-Container

Tag-Strategie

Tag-FormatBedeutungBeispiel
33-fpm Folgt Minor- und Patch-Updates in Nextcloud 33.x automatisch beim Pull nextcloud:33-fpm
34-fpm Nächste Major-Version – muss explizit gesetzt werden Änderung in all.yml nötig
1.29-alpine nginx Minor-Track; Patch-Updates kommen automatisch nginx:1.30-alpine
25.04 Collabora Release-Cycle (quartalsweise) collabora/code:25.0425.08
Patch-Update ohne Tag-Änderung Wenn Docker Hub z.B. nextcloud:33-fpm von 33.0.2 auf 33.0.3 aktualisiert, passiert auf dem Server nichts automatisch. Der Tag ist lokal gecacht. Manuell updaten:
crictl pull docker.io/library/nextcloud:33-fpm
k3s kubectl rollout restart deployment/nextcloud -n nextcloud

Nextcloud-Konfiguration

Die Konfiguration erfolgt auf zwei Wegen:

  1. custom.config.php (via Ansible ConfigMap) – statische Einstellungen, die bei jedem Playbook-Run aktualisiert werden
  2. occ-Befehle (via kubectl exec) – dynamische Einstellungen wie trusted_domains, WOPI-URL, App-Installation

custom.config.php – Übersicht der Schlüsselwerte

// Sprache
'default_language'     => 'de',
'default_locale'       => 'de_DE',
'force_language'       => 'de',

// Session (1 Stunde, kein "Angemeldet bleiben")
'session_lifetime'               => 3600,
'auto_logout'                    => true,
'remember_login_cookie_lifetime' => 0,

// Redis (Cache, Locking, Sessions)
'memcache.local'       => '\\OC\\Memcache\\Redis',
'memcache.distributed' => '\\OC\\Memcache\\Redis',
'memcache.locking'     => '\\OC\\Memcache\\Redis',
'redis' => ['host' => 'redis', 'port' => 6379],

// Reverse-Proxy
'trusted_proxies'       => ['10.42.0.0/16'],
'forwarded_for_headers' => ['HTTP_X_FORWARDED_FOR'],
'overwriteprotocol'     => 'https',

// Logging (in /data/nextcloud.log auf dem Host)
'loglevel'        => 4,
'logtimezone'     => 'Europe/Berlin',
'log_rotate_size' => 104857600,   // 100 MB

// Wartungsfenster (1 Uhr nachts)
'maintenance_window_start' => 1,

Konfiguration nach Playbook-Run prüfen

k3s kubectl exec -n nextcloud deployment/nextcloud -c nextcloud-fpm -- \
  runuser -u www-data -- php /var/www/html/occ config:list system

k3s kubectl exec -n nextcloud deployment/nextcloud -c nextcloud-fpm -- \
  runuser -u www-data -- php /var/www/html/occ setupchecks

Playbook ausführen

Vollständiger Erstlauf (neue Server-Installation)

  1. SSH-Port auf 22 setzen in inventory/host_vars/test/vars.yml: ansible_port: 22
  2. Vault-Passwörter in vault.yml eintragen und verschlüsseln
  3. Collections installieren: ansible-galaxy collection install -r requirements.yml
  4. Playbook ausführen:
    ansible-playbook nextcloud-k3s.yml --limit test --ask-vault-pass
  5. SSH-Port in vars.yml auf 10022 ändern (nach SSH-Hardening durch common_ssh)

Idempotente Wiederholung (z.B. nach Konfig-Änderung)

ansible-playbook nextcloud-k3s.yml --limit test --ask-vault-pass

Das Playbook erkennt, was bereits korrekt konfiguriert ist, und ändert nur, was nötig ist. Kubernetes-Manifeste werden nur dann neu gerollt, wenn sich das Manifest geändert hat (changed: "configured" in output).

Nur bestimmte Rollen ausführen

# Nur Nextcloud-Konfiguration (occ-Befehle)
ansible-playbook nextcloud-k3s.yml --limit test --tags next_config --ask-vault-pass

# Nur K8s-Deployments aktualisieren
ansible-playbook nextcloud-k3s.yml --limit test --tags next_k3s_deploy --ask-vault-pass

Wichtige Betriebsbefehle

Pod-Status

# Alle Pods im nextcloud-Namespace
k3s kubectl get pods -n nextcloud

# Detaillierter Status (Events bei Problemen)
k3s kubectl describe pod -l app=nextcloud -n nextcloud

# Logs
k3s kubectl logs deployment/nextcloud -c nextcloud-fpm -n nextcloud --tail=50
k3s kubectl logs deployment/nextcloud -c nginx -n nextcloud --tail=50
k3s kubectl logs deployment/collabora -n nextcloud --tail=50

occ-Befehle (Nextcloud-CLI)

# Prefix für alle occ-Befehle:
k3s kubectl exec -n nextcloud deployment/nextcloud -c nextcloud-fpm -- \
  runuser -u www-data -- php /var/www/html/occ <BEFEHL>

# Beispiele:
... occ status
... occ check
... occ setupchecks
... occ maintenance:mode --on
... occ maintenance:mode --off
... occ files:scan --all
... occ db:add-missing-indices
... occ app:list
... occ config:list system
... occ config:system:get trusted_domains

Pod neustarten

# Nextcloud (rollt graceful durch wegen Recreate-Strategie)
k3s kubectl rollout restart deployment/nextcloud -n nextcloud
k3s kubectl rollout status  deployment/nextcloud -n nextcloud --timeout=300s

# Collabora
k3s kubectl rollout restart deployment/collabora -n nextcloud

Namespace-Übersicht

k3s kubectl get all -n nextcloud

MariaDB (Host)

# Als root via Socket (Credentials in /root/.my.cnf):
mysql test_nextcloud

# DB-Dump erstellen:
mysqldump --single-transaction test_nextcloud > /tmp/dump.sql

Redis (Host)

# Verbindung testen:
redis-cli ping

# Cache leeren (z.B. nach Konfigurationsänderung):
redis-cli FLUSHALL

Monitoring

Der Monitoring-Stack (Prometheus + Grafana + Node Exporter + mysqld_exporter + php-fpm_exporter) läuft teils als Host-Dienste, teils als Sidecar-Container im Nextcloud-K3s-Pod. Grafana ist über K3s-Ingress am Nextcloud-Hostname unter /grafana/ erreichbar.

Grafana
https://nextcloud.*/grafana/

Dashboards: Node Exporter Full (1860), MySQL Overview (7362), PHP-FPM (4912)
Login: Grafana-Admin-Passwort aus vault.yml

Prometheus
127.0.0.1:9090

Nur lokal erreichbar
Retention: 30 Tage

Node Exporter
127.0.0.1:9100

CPU, RAM, Disk, Netzwerk

mysqld_exporter
127.0.0.1:9104 (Host)

Host-Dienst (common_mysqld_exporter); verbindet sich via TCP mit dem Host-MariaDB als Nutzer prometheus
Prometheus-Job: mysqld_nextcloud

php-fpm_exporter
127.0.0.1:30254 (NodePort)

Sidecar im Nextcloud-Pod; Prometheus scrapt via NodePort 30254
Prometheus-Job: php_fpm_nextcloud

MariaDB Slow Query Log

Der Host-MariaDB schreibt langsame Queries (≥ 1 Sekunde) nach /var/log/mariadb/slow.log. Die Datei wird von MariaDB automatisch angelegt und kann per journalctl oder direkt gelesen werden.

# Slow-Query-Log live lesen
tail -f /var/log/mariadb/slow.log

# Queries der letzten 24 Stunden filtern
grep -A 3 "Query_time" /var/log/mariadb/slow.log | grep -v "^--$"

# Log-Größe prüfen
ls -lh /var/log/mariadb/slow.log

Konfiguration: /etc/my.cnf.d/nextcloud-k3s.cnfslow_query_log = 1, long_query_time = 1

PHP-FPM Slow Log

PHP-FPM loggt Requests, die länger als 5 Sekunden dauern, nach /proc/1/fd/2 (stderr des Pod-Hauptprozesses). Da ptrace in Containern nicht verfügbar ist, werden keine Stack-Traces geschrieben – nur der Request-Zeitstempel.

# Slow FPM Requests verfolgen
k3s kubectl logs -n nextcloud deployment/nextcloud -c nextcloud-fpm -f | grep -i "slow"

Konfiguration: ConfigMap nextcloud-fpm-pool-configpm.status_path = /fpm-status, slowlog = /proc/1/fd/2, request_slowlog_timeout = 5s

Grafana Dashboards

Node Exporter Full (ID 1860) – Host-Metriken

Zeigt den Gesundheitszustand des gesamten Servers in Echtzeit.

PanelWas man abliest
CPU Usage Gesamtauslastung und Aufteilung je Kern (user / system / iowait). iowait > 20 % deutet auf einen Disk-Engpass hin.
Load Average (1/5/15 min) Systemlast relativ zur Anzahl CPU-Kerne. Dauerhaft über der Kernzahl → Server überlastet.
RAM / Memory Genutzter RAM, Buffers, Cache und Swap. Swap-Nutzung > 0 zeigt RAM-Mangel – Nextcloud/Collabora brauchen ggf. mehr Speicher.
Disk I/O Lese- und Schreibrate je Gerät, Latenz. Hohe Latenz bei /srv/nextcloud-www oder /data bremst Uploads/Downloads.
Disk Space Belegung der Dateisysteme. /data (User-Dateien) wächst kontinuierlich – Alarm wenn > 80 % belegt.
Network Traffic Bytes ein/aus je Interface. Ungewöhnliche Spitzen können auf Angriffe oder Datenlecks hinweisen.

MySQL Overview (ID 7362) – MariaDB-Datenbankmetriken

Zeigt die Performance der Nextcloud-Datenbank (Host-MariaDB, Port 9104).

PanelWas man abliest
Queries per Second (QPS) Datenbankaktivität. Plötzliche Spitzen können auf ineffiziente Anfragen oder Angriffe hinweisen.
Slow Queries Queries die länger als long_query_time (1 s) dauern. Dauerhaft > 0 → fehlende Indizes oder zu schwache Hardware.
InnoDB Buffer Pool Cache-Auslastung und Hit-Ratio. Hit-Ratio < 95 % → Buffer Pool zu klein, viele Lesezugriffe gehen auf die Disk (langsam).
Connections Aktive und maximale Verbindungen. Nähert sich Max → Connection-Limit erhöhen oder Abfragen optimieren.
Table Locks / Threads running Sperrkonflikte und parallele Abfragen. Viele wartende Locks = Transaktionsproblem.
Aborted Connections Unterbrochene Verbindungen (Timeout, Netzwerkfehler). Dauerhaft erhöht → Netzwerk oder App-Config prüfen.

PHP-FPM (ID 4912) – PHP-Prozesspool

Zeigt wie gut PHP-FPM die eingehenden Nextcloud-Anfragen verarbeitet (K3s-Pod, NodePort 30254).

PanelWas man abliest
Active Processes Gleichzeitig verarbeitende PHP-Worker. Erreicht dieses Panel dauerhaft pm.max_children → Pool vergrößern oder Server aufrüsten.
Idle Processes Freie Worker. Immer 0 + Queue > 0 = PHP-FPM ist überlastet.
Request Queue Wartende Requests die keinen freien Worker bekommen. Jede Zahl > 0 bedeutet Latenz für den Nutzer.
Requests per Second Durchsatz des PHP-Backends. Korreliert mit Nutzerzahlen und Sync-Clients.
Slow Requests PHP-Requests die länger als request_slowlog_timeout (5 s) dauern. Hinweis auf ineffiziente Nextcloud-Operationen (z.B. große Datei-Scans).
Max Active (Peak) Höchster Wert seit FPM-Start. Hilft bei der Dimensionierung von pm.max_children.
Grafana Dashboard-Variablen werden automatisch initialisiert Dashboard 1860 (Node Exporter Full) liegt im modernen Grafana-11-Format ohne __inputs-Sektion. Die Variablen ds_prometheus, job, nodename und node werden deshalb nach jedem Import via Python-Skript durch den common_grafana-Task gesetzt. Ohne diesen Schritt zeigen alle Panels „No data". Falls ein Dashboard leer ist: Playbook erneut ausführen oder manuell in Grafana die Dropdown-Variablen oben im Dashboard auswählen.

mysqld_exporter – Status prüfen

# Service-Status
systemctl status mysqld_exporter

# Logs
journalctl -u mysqld_exporter -n 50

# Metriken direkt abrufen (Test)
curl -s http://127.0.0.1:9104/metrics | grep mysql_up
Grafana-Hostname anpassen bei Szenariumwechsel Wenn der Server von Nextcloud auf WordPress (Blog) umgestellt wird, muss grafana_proxy_hostname in host_vars/<host>/vars.yml auf den Blog-Hostnamen geändert werden – sonst zeigt der /grafana/-Pfad auf den falschen Ingress.

Updates

Betriebssystem (AlmaLinux 9)

Security-Updates werden durch dnf-automatic täglich automatisch eingespielt. Für ein vollständiges System-Update:

  1. ssh -p 10022 root@server
    dnf upgrade -y
  2. Prüfen ob ein Reboot nötig ist:
    needs-restarting -r
  3. Bei Bedarf: Server rebooten. K3s startet automatisch nach dem Reboot, Pods kommen selbst wieder hoch.
    reboot
    # Danach prüfen:
    k3s kubectl get pods -n nextcloud

Container-Update: Patch innerhalb desselben Tags

Wenn Docker Hub z.B. nextcloud:33-fpm von 33.0.2 auf 33.0.3 aktualisiert und der Tag im Ansible unverändert bleibt:

  1. Neues Image manuell pullen:
    crictl pull docker.io/library/nextcloud:33-fpm
  2. Pod neu starten (nutzt jetzt das frisch gepullte Image):
    k3s kubectl rollout restart deployment/nextcloud -n nextcloud
    k3s kubectl rollout status  deployment/nextcloud -n nextcloud --timeout=300s
  3. DB-Migrationen sicherstellen:
    ansible-playbook nextcloud-k3s.yml --limit test --tags next_config --ask-vault-pass

Container-Update: Major-Versionssprung (z.B. 33 → 34)

Backup vor jedem Major-Update! Ein fehlgeschlagenes DB-Schema-Upgrade kann die Installation beschädigen. Immer zuerst nextcloud-backup.sh ausführen.
  1. Tag in inventory/group_vars/all.yml ändern:
    nextcloud_image_fpm: "nextcloud:34-fpm"
    # ggf. auch:
    nextcloud_image_collabora: "collabora/code:25.08"
  2. Playbook ausführen – K3s zieht das neue Image automatisch und rollt den Pod durch:
    ansible-playbook nextcloud-k3s.yml --limit test --ask-vault-pass
  3. Der Nextcloud-Container führt occ upgrade beim Start automatisch aus. Der Playbook-Lauf führt anschließend die next_config-Rolle aus (DB-Indices, WOPI-Config, App-Einstellungen).
  4. Admin-Panel auf Warnungen prüfen:
    k3s kubectl exec -n nextcloud deployment/nextcloud -c nextcloud-fpm -- \
      runuser -u www-data -- php /var/www/html/occ setupchecks
Warum kein Web-Updater? Der eingebaute Nextcloud-Web-Updater (Admin-Panel → Update-Button) funktioniert in Container-Setups nicht. Er würde versuchen, PHP-Dateien in-place zu überschreiben – aber in diesem Setup kommen die App-Dateien aus dem Docker-Image. Immer über den Image-Tag-Wechsel updaten.

Wo neue Versionen nachschauen

KomponenteURL
Nextcloudhttps://hub.docker.com/_/nextcloud/tags (Filter: *-fpm)
Collaborahttps://hub.docker.com/r/collabora/code/tags
nginxhttps://hub.docker.com/_/nginx/tags (Filter: *-alpine)
nginx-ingress (F5) Helmhttps://artifacthub.io/packages/helm/nginx-stable/nginx-ingress
cert-manager Helmhttps://artifacthub.io/packages/helm/cert-manager/cert-manager

Backup & Restore

Was gesichert wird

WasPfad (Server)Ziel (lokal)Methode
Benutzerdaten /data /data/nextcloud/<env>/data/ rsync
App-Dateien /srv/nextcloud-www /data/nextcloud/<env>/nextcloud/ rsync (data/ ausgeschlossen)
Datenbank MariaDB <env>_nextcloud (Host) /data/nextcloud/<env>/db.sql mysqldump --single-transaction
MariaDB läuft auf dem Host – nicht im Container Der Nextcloud-DB-Benutzer ist auf 'nextcloud'@'10.42.%' eingeschränkt (nur Pod-IPs). mysqldump läuft daher als root via Unix-Socket. Die Credentials stehen in /root/.my.cnf (von Ansible angelegt). Das Backup-Skript nutzt diese Verbindung automatisch.

Backup ausführen

Das Skript erwartet den Instanz-Namen als Argument. Die Instanz steuert welcher Server kontaktiert wird und wohin lokal gesichert wird.

InstanzBefehlLokales Ziel
Test-Server ./scripts/nextcloud-backup.sh test /data/nextcloud/test/
Sofie ./scripts/nextcloud-backup.sh sofie /data/nextcloud/sofie/
CVJM ./scripts/nextcloud-backup.sh cvjm /data/nextcloud/cvjm/
# Beispiel: Test-Instanz sichern
cd ~/www_k3s
./scripts/nextcloud-backup.sh test

# Ablauf (gilt für alle Instanzen):
# 1. Maintenance-Modus an  (kubectl exec occ maintenance:mode --on)
# 2. mysqldump --single-transaction → /data/nextcloud/<env>/db.sql
# 3. rsync /srv/nextcloud-www/      → /data/nextcloud/<env>/nextcloud/
# 4. rsync /data/                   → /data/nextcloud/<env>/data/
# 5. Maintenance-Modus aus

Das Skript liegt im Repository-Verzeichnis scripts/nextcloud-backup.sh. Sofie und CVJM sind erst nutzbar sobald IP und Hostname in inventory/host_vars/<env>/vars.yml eingetragen sind – das Skript prüft das und bricht mit einer Fehlermeldung ab falls noch changeme drin steht.

Restore-Workflow

Auch das Restore-Skript erwartet den Instanz-Namen als Argument:

InstanzBefehlQuelle
Test-Server ./scripts/nextcloud-restore.sh test /data/nextcloud/test/
Sofie ./scripts/nextcloud-restore.sh sofie /data/nextcloud/sofie/
CVJM ./scripts/nextcloud-restore.sh cvjm /data/nextcloud/cvjm/
  1. Server neu aufsetzen (Infrastructure) – test durch Instanz-Namen ersetzen:
    ansible-playbook nextcloud-k3s.yml --limit test --ask-vault-pass
  2. Restore-Skript ausführen (überschreibt die frische Installation):
    cd ~/www_k3s
    ./scripts/nextcloud-restore.sh test

    Ablauf: Maintenance an → Pod stoppen → rsync App-Dateien → rsync User-Daten → SQL-Import → Ownership fix → Pod starten → files:scan → Maintenance aus

  3. Ansible erneut ausführen (stellt occ-Config sicher: trusted_domains, WOPI, Apps):
    ansible-playbook nextcloud-k3s.yml --limit test --ask-vault-pass

    Dieser Schritt ist bei einem Hostname-Wechsel zwingend, sonst empfohlen.

Manuelle Einzel-Befehle (Referenz)

# Maintenance-Modus
k3s kubectl exec -n nextcloud deployment/nextcloud -c nextcloud-fpm -- \
  runuser -u www-data -- php /var/www/html/occ maintenance:mode --on

# Datenbank-Dump (auf dem Server)
mysqldump --single-transaction <db_name> > /tmp/nc.sql

# Ownership nach manuellem rsync
chown -R 33:33 /srv/nextcloud-www /data
chmod 0755 /srv/nextcloud-www && chmod 0770 /data

# File-Index neu aufbauen
k3s kubectl exec -n nextcloud deployment/nextcloud -c nextcloud-fpm -- \
  runuser -u www-data -- php /var/www/html/occ files:scan --all
instanceid nach Restore nicht ändern Die instanceid in config.php wird von Nextcloud selbst beim ersten Start gesetzt und verknüpft die App-Dateien mit dem Appdata-Verzeichnis (/data/appdata_<instanceid>/). Nach einem Restore aus dem Backup ist sie automatisch korrekt – niemals manuell überschreiben.

Troubleshooting

Pod startet nicht / bleibt in CrashLoopBackOff

k3s kubectl describe pod -l app=nextcloud -n nextcloud
k3s kubectl logs deployment/nextcloud -c nextcloud-fpm -n nextcloud --previous

Typische Ursachen:

Nextcloud-Seite lädt, aber CSS/JS fehlt (Seite sieht defekt aus)

# MIME-Typen der Assets prüfen:
curl -sI "https://nextcloud.example.com/core/css/server.css" | grep content-type
# Muss: text/css sein

curl -sI "https://nextcloud.example.com/dist/core-main.js" | grep content-type
# Muss: application/javascript sein

Ursache war in der Vergangenheit ein types {}-Block im nginx-Server-Kontext, der alle geerbten MIME-Typen überschrieben hat. Die aktuelle Konfiguration nutzt für .mjs einen dedizierten Location-Block.

Admin-Warnung: "Reverse-Proxy-Header ist falsch"

trusted_proxies fehlt in custom.config.php oder enthält nicht den K3s Pod-CIDR (10.42.0.0/16).

k3s kubectl exec -n nextcloud deployment/nextcloud -c nextcloud-fpm -- \
  runuser -u www-data -- php /var/www/html/occ config:system:get trusted_proxies

Collabora: "Unautorisierter WOPI-Host"

Die wopi_allowlist enthält nicht den Pod-CIDR (10.42.x.x). Collabora-Pods verbinden sich zum Nextcloud-WOPI von ihrer Pod-IP aus.

k3s kubectl exec -n nextcloud deployment/nextcloud -c nextcloud-fpm -- \
  runuser -u www-data -- php /var/www/html/occ \
  config:app:get richdocuments wopi_allowlist

# Korrektur:
k3s kubectl exec -n nextcloud deployment/nextcloud -c nextcloud-fpm -- \
  runuser -u www-data -- php /var/www/html/occ \
  config:app:set richdocuments wopi_allowlist \
  --value="10.42.0.0/16,10.43.0.0/16,collabora.example.de,82.165.165.230"

Zertifikat wird nicht ausgestellt

# cert-manager Logs prüfen:
k3s kubectl logs -n cert-manager deployment/cert-manager --tail=50

# Certificate-Objekt prüfen:
k3s kubectl describe certificate nextcloud-tls -n nextcloud

# ClusterIssuer-Status:
k3s kubectl describe clusterissuer letsencrypt-prod

Häufige Ursache: Port 80 ist durch iptables oder den Provider blockiert. cert-manager benötigt Port 80 für den HTTP-01-Challenge.

CronJob läuft nicht

# Status der letzten Jobs:
k3s kubectl get cronjob nextcloud-cron -n nextcloud
k3s kubectl get jobs -n nextcloud --sort-by=.metadata.creationTimestamp | tail -5

# Logs des letzten Job-Pods:
k3s kubectl logs -n nextcloud -l job-name=nextcloud-cron-<id>

Sicherheit

Mehrschichtige Schutzstrategie

EbeneMaßnahmeRolle
Host-Netzwerk iptables IPv4/IPv6 – Default-Policy DROP, Portscan-Blocker, ICMP-Rate-Limit, nur 80/443/10022 offen common_firewall
SSH Port 10022, Key-only, gehärtete sshd_config, Fail2Ban common_ssh, common_fail2ban
HTTP Rate-Limiting Login (5r/s), HSTS, Security-Header common_k3s (nginx-ingress (F5) Helm-Values + Ingress-Annotations)
Datenbank User nur aus Pod-CIDR (10.42.%), MariaDB bind 0.0.0.0 + iptables next_mariadb, common_firewall
Redis Bind auf localhost + Host-IP, iptables auf Pod-CIDR next_redis, common_firewall
SELinux Enforcing, container_file_t für HostPath-Volumes next_selinux
Audit auditd mit Hardening-Regeln (sudo, SSH, Cron, Kernelmodule) common_auditd
Rootkits rkhunter täglicher Scan um 03:15 common_rkhunter
Patches dnf-automatic: Security-Updates täglich automatisch common_dnf_automatic
Secrets Ansible Vault, K8s Secrets (Opaque), no_log auf sensible Tasks alle Rollen

Firewall im Detail

Das Skript /root/fw/fw_ipv4.sh (aus common_firewall) wird per @reboot-Cronjob nach jedem Neustart angewendet. K3s fügt danach seine eigenen KUBE-*-Chains oben in die INPUT/OUTPUT-Chains ein.

FeatureDetail
Default-Policy DROP INPUT, OUTPUT und FORWARD werden sofort nach dem Flush auf DROP gesetzt – kein Fenster, in dem ungeschützter Traffic durchkommt
Portscan-Blocker Jedes Paket, das keine ACCEPT-Regel trifft, trägt die Quell-IP in /proc/net/xt_recent/portscan ein. Beim nächsten Paket dieser IP greift --rcheck ganz oben → 24h Sperre. Prüfen: cat /proc/net/xt_recent/portscan
ICMP Rate-Limit Echo-Request max. 5/s, Burst 10 – schützt gegen ICMP-Flood von vielen Quellen. Bestehende Ping-Sessions laufen über ESTABLISHED weiter.
Kein server_ipv4-Whitelist Die frühere Regel -s server_ipv4 -j ACCEPT wurde entfernt – externe Pakete mit gespoofter Server-IP wurden dadurch akzeptiert. Lokaler Traffic läuft sicher über Loopback (-i lo -j ACCEPT).
K3s OUTPUT-CIDR OUTPUT erlaubt explizit Traffic zu Pod-CIDR (10.42.0.0/16) und Service-CIDR (10.43.0.0/16) – nötig für Kubelet-Probes, kube-proxy, Metrics-Server. Ohne diese Regeln bricht K3s-intern Port 10250.
IPv6 DROP-Policy ip6tables startet direkt mit Policy DROP, ICMPv6 rate-limited, echo-reply nur für ESTABLISHED/RELATED
# Geblockte Pakete live verfolgen
journalctl -k -f --grep "IPTables-Dropped"

# Portscan-Blockliste anzeigen
cat /proc/net/xt_recent/portscan | awk '{print $1}'

# Firewall-Regeln neu laden (nach Reboot passiert das automatisch)
/root/fw/fw_ipv4.sh && /root/fw/fw_ipv6.sh

Wichtige Sicherheitshinweise

vault.yml niemals unverschlüsselt committen Vor jedem git commit prüfen: ansible-vault encrypt inventory/host_vars/test/vault.yml
MariaDB-Bind auf 0.0.0.0 MariaDB lauscht auf allen Interfaces, damit Pods via Host-IP verbinden können. Der Schutz erfolgt ausschließlich durch iptables (nur Pod-CIDR erlaubt) und den DB-User-Grant ('nextcloud'@'10.42.%'). Niemals Port 3306 öffentlich freigeben.
StrictHostKeyChecking=no in ansible.cfg Deaktiviert die Host-Key-Prüfung beim SSH-Verbindungsaufbau. Geeignet für Testserver mit wechselnden IPs, aber nicht für Produktionsserver empfohlen. Für Produktion: StrictHostKeyChecking=accept-new setzen.