WordPress – Betriebsdokumentation
WordPress 6.9 auf K3s (AlmaLinux 9) · Letzte Aktualisierung: Mai 2026
Überblick
WordPress läuft auf einem Single-Node-K3s-Cluster auf AlmaLinux 9.
Die gesamte Infrastruktur wird mit einem Ansible-Playbook
(blog.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.
/srv/wordpress-db).
Es gibt keine Hochverfügbarkeit. Updates verursachen eine kurze Ausfallzeit
(typisch: 30–60 Sekunden).
Architektur
Gesamtüberblick
Warum dieser Ansatz?
| Komponente | Wo läuft es? | Begründung |
|---|---|---|
| WordPress PHP-FPM + nginx | K3s Pod Container | Einfache Updates durch Image-Wechsel; kein php-fpm auf dem Host |
| MariaDB | K3s Pod Container | Anders als bei Nextcloud läuft die DB hier im Container; Daten auf HostPath |
| 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 |
| WP-Cron | K3s CronJob hostNetwork | Ersetzt den WordPress-internen Pseudo-Cron; läuft zuverlässig alle 5 Minuten |
WordPress-Pod im Detail
Container 1: wordpress-fpm
Das offizielle wordpress:6.9-fpm-Image. Führt PHP-FPM auf Port 9000 aus.
Beim ersten Start installiert der Docker-Entrypoint WordPress automatisch anhand
der Umgebungsvariablen (DB-Host, DB-Name, DB-User, DB-Passwort).
WordPress-Dateien werden in /var/www/html auf dem gemeinsamen
HostPath-Volume abgelegt.
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 – dieser nginx spricht nur plain HTTP.
127.0.0.1:9000 mit PHP-FPM kommunizieren,
ohne einen separaten Kubernetes-Service zu benötigen. Außerdem lesen beide
Container WordPress-Dateien aus demselben Volume – nginx für statische Assets,
PHP-FPM für PHP-Skripte.
Speicher & Daten
| Pfad auf dem Host | Mountpoint im Container | Inhalt |
|---|---|---|
/srv/wordpress-www |
/var/www/html |
WordPress PHP-Core, wp-content (Themes, Plugins, Uploads), wp-config.php |
/srv/wordpress-db |
/var/lib/mysql |
MariaDB-Datenbankdateien (InnoDB, Systemtabellen) |
/srv/wordpress-www/wp-content/uploads/.
Dieses Verzeichnis muss in das Backup eingeschlossen werden – es wächst mit der Zeit
und ist nicht im Container-Image enthalten.
Warum sind App und Datenbank getrennte Volumes?
- Datenbankdaten (
/srv/wordpress-db) können unabhängig gesichert werden - WordPress-Dateien werden bei einem Update durch das neue Image ergänzt (der Entrypoint aktualisiert Core-Dateien in-place)
- Beide Volumes überleben einen Pod-Neustart oder ein Image-Update unbeschädigt
Ownership und Berechtigungen
WordPress-FPM läuft als www-data (UID 33 im Container).
Das HostPath-Verzeichnis muss für diesen Benutzer schreibbar sein:
# Ownership für WordPress-Dateien setzen (vom Host):
chown -R 33:33 /srv/wordpress-www
# Datenbankdaten gehören root (MariaDB im Container nutzt root intern):
ls -la /srv/wordpress-db
Netzwerk & TLS
IP-Adressbereiche
| Bereich | Zweck | Relevant für |
|---|---|---|
10.42.0.0/16 |
K3s Pod-CIDR (Flannel) | iptables-Regeln, set_real_ip_from im nginx-Sidecar |
10.43.0.0/16 |
K3s Service-CIDR | ClusterIP-Adressen der K8s-Services |
217.154.101.78 |
Öffentliche Host-IP | DNS, MariaDB-Service-Endpoint |
Benötigte DNS-Einträge
Vor dem ersten Playbook-Lauf muss der DNS-Eintrag beim Domain-Registrar angelegt sein. cert-manager benötigt ihn für den Let's Encrypt HTTP-01-Challenge – ohne gültigen Eintrag schlägt die Zertifikatsausstellung fehl.
| Name (Hostname) | Typ | Wert | Zweck |
|---|---|---|---|
www.apt-upgrade.me |
A | Öffentliche IP des Servers (217.154.101.78) |
WordPress-Blog, Let's Encrypt TLS |
fw_ipv6.sh) sind bereits konfiguriert.
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)
nginx-ingress leitet Anfragen aus seinem Pod-Namespace (10.42.x.x) weiter.
Der nginx-Sidecar ist so konfiguriert, dass er die echte Client-IP aus dem
X-Forwarded-For-Header liest:
# nginx-ConfigMap (nginx-cm.yml.j2):
set_real_ip_from 10.42.0.0/16; # Pod-CIDR – dort kommen ingress-Pakete her
real_ip_header X-Forwarded-For;
real_ip_recursive on;
Rate-Limiting und Sicherheit am Ingress
Schutzmaßnahmen sind direkt im Ingress über nginx.org/-Annotations definiert:
| Maßnahme | Annotation / Konfiguration | Wert |
|---|---|---|
| HTTPS-Redirect | nginx.org/ssl-redirect |
true – HTTP wird zu HTTPS weitergeleitet |
| xmlrpc.php blockieren | nginx.org/server-snippets |
deny all; return 403; – verhindert XML-RPC-Brute-Force |
| wp-login.php Rate-Limit | nginx.org/server-snippets |
5 r/s pro IP, Burst 5 – verhindert Login-Brute-Force |
| Security-Header | nginx.org/location-snippets |
HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy |
| Upload-Limit | nginx.org/client-max-body-size |
500m – für Medienuploads über das WP-Backend |
wp_login wird in den Helm-Values des nginx-ingress-Controllers
(http-snippets) definiert und ist damit global für alle Ingress-Ressourcen
verfügbar. Die eigentliche Anwendung der Zone erfolgt über die
server-snippets-Annotation im WordPress-Ingress.
Wichtige Dateien im Repository
Inventory & Variablen
| Datei | Inhalt |
|---|---|
inventory/group_vars/all.yml |
Globale Einstellungen: Container-Image-Tags, Helm-Chart-Versionen, Monitoring-Versionen, K3s-CIDRs |
inventory/host_vars/www.apt-upgrade.me/vars.yml |
Hostspezifisch: Hostname, IP, DB-Name, DB-User, Pfade, Plugins, Mail-Config |
inventory/host_vars/www.apt-upgrade.me/vault.yml |
Verschlüsselt! Passwörter: DB-Root, DB-User, WP-Admin, SMTP.
Vor git-Commit verschlüsseln:
ansible-vault encrypt inventory/host_vars/www.apt-upgrade.me/vault.yml |
Ansible-Rollen (WordPress-spezifisch)
| Rolle | Aufgabe |
|---|---|
blog_packages | Pakete installieren (z.B. SELinux-Tools) |
blog_selinux | SELinux enforcing, container_file_t für HostPath-Verzeichnisse |
common_k3s | K3s, Helm, cert-manager, nginx-ingress (F5) – gemeinsame Basis |
blog_k3s_deploy | K8s-Manifeste: Namespace, Secrets, ConfigMaps, Deployments, Ingress, CronJob |
blog_wordpress | Post-Deploy: WP-CLI-Befehle (Admin, Plugins, Einstellungen) |
Templates (erzeugen die K8s-Manifeste)
| Template | Erzeugt | Wichtig weil |
|---|---|---|
wordpress-deployment.yml.j2 |
Deployment + Service für WordPress | Image-Tags, Volumes, Env-Variablen (DB-Credentials als Secret-Ref) |
mariadb-deployment.yml.j2 |
Deployment + Service für MariaDB | Datenbankinitialisierung beim ersten Start, HostPath-Volume |
nginx-cm.yml.j2 |
ConfigMap wordpress-nginx-config |
nginx-Routing für WordPress, PHP-FastCGI, Real-IP aus X-Forwarded-For |
secret.yml.j2 |
Secret wordpress-secrets |
DB-Host, DB-Name, DB-User, DB-Passwort – als Env-Variablen in den Pod |
ingress.yml.j2 |
Ingress wordpress |
TLS, Rate-Limiting, xmlrpc.php-Block, Grafana-Sub-Pfad, Security-Header |
cronjob.yml.j2 |
K8s CronJob wordpress-cron |
Führt wp-cron.php alle 5 Minuten aus; ersetzt WP-internen Pseudo-Cron |
Variablen & Secrets
Wo Credentials liegen
Alle Passwörter stehen in inventory/host_vars/www.apt-upgrade.me/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/www.apt-upgrade.me/vault.yml
# Direkt im Editor öffnen (bevorzugt):
ansible-vault edit inventory/host_vars/www.apt-upgrade.me/vault.yml
Typischer Inhalt vault.yml
blog_db_root_pass: "..." # MariaDB root-Passwort
blog_db_pass: "..." # WordPress DB-Benutzer-Passwort
blog_admin_pass: "..." # WordPress Admin-Passwort
mail_smtppass: "..." # SMTP-Passwort für Mail-Versand
grafana_admin_pass: "..." # Grafana Admin-Passwort
Secrets im K8s-Cluster
Die Vault-Variablen fließen beim Playbook-Run in das K8s-Secret
wordpress-secrets im Namespace wordpress.
# Secret anzeigen (dekodiert):
k3s kubectl get secret wordpress-secrets -n wordpress \
-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. K3s zieht automatisch ein neues Image nur dann,
wenn der Tag im Manifest geändert wurde (imagePullPolicy: IfNotPresent).
# inventory/group_vars/all.yml
blog_image_wordpress: "wordpress:6.9-fpm" # WordPress PHP-FPM
blog_image_mariadb: "mariadb:10.11" # MariaDB LTS
blog_image_nginx: "nginx:1.30-alpine" # nginx Sidecar
Tag-Strategie
| Tag-Format | Bedeutung | Beispiel |
|---|---|---|
6.9-fpm |
Folgt Patch-Updates in WordPress 6.9.x automatisch beim Pull | wordpress:6.9-fpm |
10.11 |
MariaDB LTS-Branch; Patch-Updates kommen automatisch, Major-Upgrade erfordert Migration | mariadb:10.11 (LTS bis Juni 2028) |
1.30-alpine |
nginx Stable-Branch; kleinere Patches kommen automatisch | nginx:1.30-alpine |
Neue Versionen prüfen
| Komponente | URL |
|---|---|
| WordPress | https://hub.docker.com/_/wordpress/tags (Filter: *-fpm) |
| MariaDB | https://hub.docker.com/_/mariadb/tags |
| nginx | https://hub.docker.com/_/nginx/tags (Filter: *-alpine) |
| nginx-ingress (F5) Helm | https://artifacthub.io/packages/helm/nginx-stable/nginx-ingress |
| cert-manager Helm | https://artifacthub.io/packages/helm/cert-manager/cert-manager |
wordpress:6.9-fpm auf 6.9.1 aktualisiert,
passiert auf dem Server nichts automatisch. Der Tag ist lokal gecacht.
Manuell updaten:
crictl pull docker.io/library/wordpress:6.9-fpm
k3s kubectl rollout restart deployment/wordpress -n wordpress
WordPress-Konfiguration
wp-config.php
Die wp-config.php wird beim ersten Start vom WordPress-Docker-Entrypoint
automatisch aus den Umgebungsvariablen erzeugt. Wichtige generierte Einträge:
define('DB_HOST', 'mariadb'); // K8s Service-Name im Namespace
define('DB_NAME', 'wordpress_db');
define('DB_USER', 'wordpress');
define('DB_PASSWORD', '...'); // aus K8s Secret
// WordPress-eigener Cron ist deaktiviert (K8s CronJob übernimmt):
define('DISABLE_WP_CRON', true);
// URL-Einstellungen (von Ansible gesetzt):
define('WP_HOME', 'https://www.apt-upgrade.me');
define('WP_SITEURL', 'https://www.apt-upgrade.me');
DISABLE_WP_CRON = true), weil ein
dedizierter K8s-CronJob alle 5 Minuten wp-cron.php aufruft.
Das ist zuverlässiger und belastet den normalen Seitenaufruf nicht.
Plugins (via Ansible verwaltet)
Plugins werden in inventory/host_vars/www.apt-upgrade.me/vars.yml
als Liste definiert und vom blog_wordpress-Playbook automatisch
installiert und aktiviert:
blog_wordpress_plugins:
- all-in-one-wp-migration # Import/Export für Backups
- classic-editor # Klassischer Editor statt Gutenberg
- limit-login-attempts-reloaded # Zusätzlicher Brute-Force-Schutz
Datenbank direkt abfragen
# Im MariaDB-Pod (kein Root-Passwort nötig, da socket auth):
k3s kubectl exec -n wordpress deployment/mariadb -- \
mysql -u wordpress -p wordpress_db
# Oder mit explizitem Passwort:
k3s kubectl exec -n wordpress deployment/mariadb -- \
mysql -u wordpress -p'<passwort>' wordpress_db -e "SHOW TABLES;"
Playbook ausführen
Vollständiger Erstlauf (neue Server-Installation)
-
SSH-Port auf
22setzen ininventory/host_vars/www.apt-upgrade.me/vars.yml:ansible_port: 22 -
Vault-Passwörter in
vault.ymleintragen und verschlüsseln -
Collections installieren:
ansible-galaxy collection install -r requirements.yml -
Playbook ausführen:
ansible-playbook blog.yml --limit www.apt-upgrade.me --ask-vault-pass -
SSH-Port in
vars.ymlauf10022ändern (nach SSH-Hardening durchcommon_ssh)
Idempotente Wiederholung (z.B. nach Konfig-Änderung)
ansible-playbook blog.yml --limit www.apt-upgrade.me --ask-vault-pass
Nur bestimmte Rollen ausführen
# Nur K8s-Deployments aktualisieren
ansible-playbook blog.yml --limit www.apt-upgrade.me --tags blog_k3s_deploy --ask-vault-pass
# Nur WordPress-Konfiguration (Plugins, Admin, Einstellungen)
ansible-playbook blog.yml --limit www.apt-upgrade.me --tags blog_wordpress --ask-vault-pass
Wichtige Betriebsbefehle
Pod-Status
# Alle Pods im wordpress-Namespace
k3s kubectl get pods -n wordpress
# Detaillierter Status
k3s kubectl describe pod -l app=wordpress -n wordpress
# Logs WordPress-FPM
k3s kubectl logs deployment/wordpress -c wordpress-fpm -n wordpress --tail=50
# Logs nginx-Sidecar
k3s kubectl logs deployment/wordpress -c nginx -n wordpress --tail=50
# Logs MariaDB
k3s kubectl logs deployment/mariadb -n wordpress --tail=50
WP-CLI-Befehle
# Prefix für alle WP-CLI-Befehle:
k3s kubectl exec -n wordpress deployment/wordpress -c wordpress-fpm -- \
wp --allow-root <BEFEHL>
# Beispiele:
... wp core version
... wp core check-update
... wp plugin list
... wp plugin update --all
... wp theme list
... wp cache flush
... wp cron event list
... wp user list
... wp db check
... wp search-replace 'http://www.apt-upgrade.me' 'https://www.apt-upgrade.me' --dry-run
Pod neustarten
# WordPress
k3s kubectl rollout restart deployment/wordpress -n wordpress
k3s kubectl rollout status deployment/wordpress -n wordpress --timeout=300s
# MariaDB (Achtung: kurze DB-Downtime!)
k3s kubectl rollout restart deployment/mariadb -n wordpress
Namespace-Übersicht
k3s kubectl get all -n wordpress
Ingress und Zertifikat prüfen
# Ingress-Ressource
k3s kubectl get ingress -n wordpress
k3s kubectl describe ingress wordpress -n wordpress
# TLS-Zertifikat
k3s kubectl get certificate -n wordpress
k3s kubectl describe certificate wordpress-tls -n wordpress
Monitoring
Der Monitoring-Stack (Prometheus + Grafana + Node Exporter + mysqld_exporter + php-fpm_exporter)
läuft teils als Host-Dienste, teils als Sidecar-Container in K3s-Pods.
Grafana ist über den Ingress am Blog-Hostname unter /grafana/ erreichbar.
https://www.apt-upgrade.me/grafana/
Dashboards: Node Exporter Full (1860), MySQL Overview (7362), PHP-FPM (4912)
Login: Grafana-Admin-Passwort aus vault.yml
127.0.0.1:9090
Nur lokal erreichbar
Retention: 30 Tage
127.0.0.1:9100CPU, RAM, Disk, Netzwerk
127.0.0.1:30104 (NodePort)
Sidecar im MariaDB-Pod; Prometheus scrapt via NodePort 30104
Prometheus-Job: mysqld_wordpress
127.0.0.1:30253 (NodePort)
Sidecar im WordPress-Pod; Prometheus scrapt via NodePort 30253
Prometheus-Job: php_fpm_wordpress
MariaDB Slow Query Log
Der MariaDB-Pod schreibt langsame Queries (≥ 1 Sekunde) nach
/var/lib/mysql/slow.log – das liegt im HostPath-Volume und ist auf dem Host
unter /srv/wordpress-db/slow.log lesbar.
(Hintergrund: /dev/stderr ist im MariaDB-Container unter K3s nicht beschreibbar –
der mysql-Prozess erhält uid 999 und hat keinen Zugriff auf den fd.)
# Slow Queries auf dem Host lesen
tail -f /srv/wordpress-db/slow.log
# Oder im Pod direkt
k3s kubectl exec -n wordpress deployment/mariadb -c mariadb -- \
tail -f /var/lib/mysql/slow.log
Konfiguration: MariaDB-Pod-Args --slow-query-log=1 --slow-query-log-file=/var/lib/mysql/slow.log --long-query-time=1
PHP-FPM Slow Log
PHP-FPM loggt Requests, die länger als 5 Sekunden dauern, nach /proc/1/fd/2
(entspricht stderr des Pod-Hauptprozesses). Da ptrace in Containern nicht
verfügbar ist, werden keine Stack-Traces geschrieben – nur der Request-Zeitstempel und die
Ausführungszeit.
# Slow FPM Requests verfolgen
k3s kubectl logs -n wordpress deployment/wordpress -c wordpress-fpm -f | grep -i "slow"
Konfiguration: ConfigMap wordpress-fpm-pool-config →
pm.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.
| Panel | Was 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 – WordPress/MariaDB brauchen ggf. mehr Speicher. |
| Disk I/O | Lese- und Schreibrate je Gerät, Latenz.
Hohe Latenz bei /srv/wordpress-www oder /srv/wordpress-db bremst die Seite. |
| Disk Space | Belegung der Dateisysteme. /srv/wordpress-www (Uploads) wächst mit der Zeit –
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 WordPress-Datenbank (MariaDB-Sidecar im K3s-Pod, NodePort 30104).
| Panel | Was man abliest |
|---|---|
| Queries per Second (QPS) | Datenbankaktivität. Plötzliche Spitzen können auf ineffiziente Plugins oder Angriffe hinweisen. |
| Slow Queries | Queries die länger als 1 s dauern. Dauerhaft > 0 → fehlende Indizes (häufig bei schlecht programmierten Plugins). |
| 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. Bei WordPress typisch niedrig (< 10). |
| Table Locks / Threads running | Sperrkonflikte. Bei WordPress können schlecht optimierte Plugins Lock-Probleme verursachen. |
| Aborted Connections | Unterbrochene Verbindungen. Bei WordPress-Pod-Restarts erwartet; dauerhaft erhöht = Problem. |
PHP-FPM (ID 4912) – PHP-Prozesspool
Zeigt wie gut PHP-FPM die WordPress-Anfragen verarbeitet (K3s-Pod, NodePort 30253).
| Panel | Was man abliest |
|---|---|
| Active Processes | Gleichzeitig verarbeitende PHP-Worker. Erreicht dieses Panel dauerhaft
pm.max_children → Pool vergrößern oder Plugin optimieren. |
| Idle Processes | Freie Worker. Immer 0 + Queue > 0 = PHP-FPM ist überlastet. |
| Request Queue | Wartende Requests. Jede Zahl > 0 bedeutet spürbare Ladezeit für den Seitenbesucher. |
| Requests per Second | Durchsatz. Nützlich um Traffic-Spitzen (z.B. nach einem Beitrag) zu erkennen. |
| Slow Requests | PHP-Requests > 5 s. Häufig: schlecht optimierte Plugins, externe API-Calls oder fehlende Indizes. |
| Max Active (Peak) | Höchster Wert seit FPM-Start. Hilft bei der Dimensionierung von pm.max_children. |
__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 – Konfigurationshinweis
DATA_SOURCE_NAME nicht mehr unterstützt. Der Exporter liest
Credentials ausschließlich aus einer .my.cnf-Datei (--config.my-cnf).
Im WordPress-Setup werden die Credentials als Key mysqld-exporter-cnf im
K8s-Secret wordpress-secrets gespeichert und mit defaultMode: 0444
in den mysqld_exporter-Sidecar-Container eingemountet
(der Exporter läuft als User nobody – 0600 wäre Permission denied).
Prometheus Scrape-Ziele
# Aktive Scrape-Ziele prüfen (im Browser oder curl)
curl -s http://127.0.0.1:9090/api/v1/targets | python3 -m json.tool | grep -E '"job"|"state"|"scrapeUrl"'
WP-Cron
WordPress benötigt einen regelmäßigen Cron-Aufruf für geplante Aufgaben: Plugin-Updates, E-Mail-Versand, Veröffentlichung geplanter Beiträge etc. In diesem Setup übernimmt ein Kubernetes CronJob diese Aufgabe.
wp-cron.php
ohne kubectl-Umwege. Ein K8s-CronJob ist der saubere Weg: er startet alle
5 Minuten einen temporären Pod im selben Namespace, der direkten Zugriff auf
das WordPress-Volume und die Datenbank hat.
CronJob-Status prüfen
# Status und letzter Lauf
k3s kubectl get cronjob wordpress-cron -n wordpress
# Die letzten 5 Jobs (abgeschlossene Pods)
k3s kubectl get jobs -n wordpress --sort-by=.metadata.creationTimestamp | tail -5
# Logs eines CronJob-Pods (ID aus vorherigem Befehl):
k3s kubectl logs -n wordpress -l job-name=wordpress-cron-<id>
Manuelle Ausführung (für Tests)
k3s kubectl exec -n wordpress deployment/wordpress -c wordpress-fpm -- \
php /var/www/html/wp-cron.php
Updates
Betriebssystem (AlmaLinux 9)
Security-Updates werden durch dnf-automatic täglich automatisch
eingespielt. Für ein vollständiges System-Update:
-
ssh -p 10022 root@217.154.101.78 dnf upgrade -y -
Prüfen ob ein Reboot nötig ist:
needs-restarting -r -
Bei Bedarf: Server rebooten. K3s startet automatisch, alle Pods kommen selbst wieder hoch.
reboot # Danach prüfen: k3s kubectl get pods -n wordpress
WordPress-Update: Patch innerhalb desselben Tags
Wenn Docker Hub wordpress:6.9-fpm auf 6.9.1 aktualisiert
und der Tag im Ansible unverändert bleibt:
- Neues Image manuell pullen:
crictl pull docker.io/library/wordpress:6.9-fpm - Pod neu starten:
k3s kubectl rollout restart deployment/wordpress -n wordpress k3s kubectl rollout status deployment/wordpress -n wordpress --timeout=300s
WordPress-Update: Minor-Versionssprung (z.B. 6.9 → 7.0)
- Tag in
inventory/group_vars/all.ymländern:blog_image_wordpress: "wordpress:7.0-fpm" - Playbook ausführen – K3s zieht das neue Image und rollt den Pod durch:
ansible-playbook blog.yml --limit www.apt-upgrade.me --ask-vault-pass - WordPress-DB-Update prüfen (bei Major-Versionen manchmal nötig):
k3s kubectl exec -n wordpress deployment/wordpress -c wordpress-fpm -- \ wp --allow-root core update-db
MariaDB-Update
Patch-Updates innerhalb von 10.11:
crictl pull docker.io/library/mariadb:10.11
k3s kubectl rollout restart deployment/mariadb -n wordpress
Backup & Restore
Was gesichert wird
| Was | Pfad (Server) | Ziel (lokal) | Methode |
|---|---|---|---|
| WordPress-Dateien + Uploads | /srv/wordpress-www |
/data/wordpress/www.apt-upgrade.me/wordpress/ |
rsync |
| Datenbank | MariaDB-Pod wordpress_db |
/data/wordpress/www.apt-upgrade.me/db.sql |
mysqldump via kubectl exec |
kubectl exec in den mariadb-Container ausgeführt.
Die Root-Credentials kommen aus der MARIADB_ROOT_PASSWORD-Umgebungsvariable,
die vom K8s-Secret injiziert wird – kein Passwort im Skript nötig.
Backup ausführen
Das Backup-Skript hat kein Umgebungs-Argument – es sichert immer
www.apt-upgrade.me nach /data/wordpress/www.apt-upgrade.me/.
# Aus dem Repository-Verzeichnis aufrufen:
cd ~/www_k3s
./scripts/wordpress-backup.sh
# Ablauf:
# 1. WP-CLI maintenance-mode activate (im wordpress-fpm Container)
# 2. kubectl exec → mysqldump (im mariadb-Container, Passwort aus K8s-Secret)
# 3. rsync /srv/wordpress-www/ → /data/wordpress/www.apt-upgrade.me/wordpress/
# 4. WP-CLI maintenance-mode deactivate
Restore-Workflow
-
Server neu aufsetzen (Infrastructure):
ansible-playbook blog.yml --limit www.apt-upgrade.me --ask-vault-pass -
Restore-Skript ausführen:
cd ~/www_k3s ./scripts/wordpress-restore.shAblauf: Maintenance an → WordPress + MariaDB stoppen → rsync WordPress-Dateien → MariaDB-Daten-Dir leeren → MariaDB starten (Neu-Initialisierung aus K8s-Secret) → SQL-Dump importieren → Ownership fix → WordPress starten → Maintenance aus
-
Ansible erneut ausführen (stellt Plugin-Liste und WP-Config sicher):
ansible-playbook blog.yml --limit www.apt-upgrade.me --ask-vault-pass
/srv/wordpress-db/ bewusst, damit die MariaDB beim
nächsten Start ihre Datenbank frisch aus den K8s-Secret-Umgebungsvariablen initialisiert
(MARIADB_DATABASE, MARIADB_USER, MARIADB_PASSWORD).
Danach wird der SQL-Dump importiert. Ohne diesen Schritt würde ein alter, inkompatibler
Datenbankzustand geladen.
Manuelle Einzel-Befehle (Referenz)
# mysqldump aus dem MariaDB-Pod (MARIADB_ROOT_PASSWORD aus K8s-Secret)
k3s kubectl exec -n wordpress deployment/mariadb -c mariadb -- \
sh -c 'mysqldump --single-transaction wordpress_db \
-u root -p"$MARIADB_ROOT_PASSWORD"'
# SQL-Dump in laufenden MariaDB-Pod importieren
k3s kubectl exec -n wordpress deployment/mariadb -c mariadb -- \
sh -c 'mysql wordpress_db -u root -p"$MARIADB_ROOT_PASSWORD" < /tmp/dump.sql'
# WP-CLI Maintenance
k3s kubectl exec -n wordpress deployment/wordpress -c wordpress-fpm -- \
php /var/www/html/wp-cli.phar maintenance-mode activate --path=/var/www/html --allow-root
Troubleshooting
WordPress-Pod startet nicht / CrashLoopBackOff
k3s kubectl describe pod -l app=wordpress -n wordpress
k3s kubectl logs deployment/wordpress -c wordpress-fpm -n wordpress --previous
Typische Ursachen:
- Datenbank nicht erreichbar: MariaDB-Pod läuft nicht.
k3s kubectl get pods -n wordpressprüfen. - Falsches DB-Passwort: Secret
wordpress-secretsstimmt nicht mit dem MariaDB-Benutzer überein. Playbook neu ausführen. - Volume-Berechtigungen:
chown -R 33:33 /srv/wordpress-wwwund Pod neu starten.
MariaDB-Pod startet nicht
k3s kubectl logs deployment/mariadb -n wordpress --previous
- Datenbankdateien beschädigt: MariaDB versucht eine Recovery. Im schlimmsten Fall müssen die Daten aus dem Backup wiederhergestellt werden.
- Volume-Berechtigungen:
MariaDB erwartet, dass
/srv/wordpress-dbfür den internen mysql-User (UID 999) schreibbar ist. Ansible setzt die Berechtigungen automatisch.
WordPress-Seite zeigt 502 Bad Gateway
nginx-Sidecar kann PHP-FPM nicht erreichen. Prüfen:
# Läuft PHP-FPM?
k3s kubectl logs deployment/wordpress -c wordpress-fpm -n wordpress --tail=20
# Sind beide Container im Pod Ready?
k3s kubectl get pod -l app=wordpress -n wordpress
WordPress-Admin-Backend: "Es sind Datenbankaktualisierungen erforderlich"
k3s kubectl exec -n wordpress deployment/wordpress -c wordpress-fpm -- \
wp --allow-root core update-db
Zertifikat wird nicht ausgestellt
# cert-manager Logs:
k3s kubectl logs -n cert-manager deployment/cert-manager --tail=50
# Certificate-Objekt:
k3s kubectl describe certificate wordpress-tls -n wordpress
# 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 (auch wenn die Site nur über HTTPS erreichbar sein soll).
Rate-Limiting greift zu aggressiv (eigene IP gesperrt)
# Ingress-Logs prüfen (nginx-ingress-Controller):
k3s kubectl logs -n ingress-nginx daemonset/ingress-nginx-nginx-ingress-controller --tail=50
# Temporär: Ingress ohne Rate-Limit neu deployen (für Notfälle):
# → nginx.org/server-snippets Annotation im Ingress entfernen und Playbook ausführen
WP-Cron läuft nicht
# CronJob-Status:
k3s kubectl get cronjob wordpress-cron -n wordpress
# Letzter Job-Pod (sollte "Completed" sein):
k3s kubectl get pods -n wordpress --sort-by=.metadata.creationTimestamp | tail -5
# Manuell auslösen (für Test):
k3s kubectl exec -n wordpress deployment/wordpress -c wordpress-fpm -- \
php /var/www/html/wp-cron.php
Sicherheit
Mehrschichtige Schutzstrategie
| Ebene | Maßnahme | Rolle |
|---|---|---|
| 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 | wp-login.php: 5 r/s pro IP, Burst 5 | common_k3s (nginx-ingress Helm-Values + Ingress-Annotations) |
| HTTP – Endpoint-Block | xmlrpc.php → 403 (verhindert XML-RPC-Brute-Force) | blog_k3s_deploy (Ingress server-snippets) |
| HTTP – Security-Header | HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy | blog_k3s_deploy (Ingress location-snippets) |
| WordPress | Limit Login Attempts Plugin, Plugin-Updates via Ansible | blog_wordpress |
| SELinux | Enforcing, container_file_t für HostPath-Volumes |
blog_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.
| Feature | Detail |
|---|---|
| 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
git commit prüfen:
ansible-vault encrypt inventory/host_vars/www.apt-upgrade.me/vault.yml
wp plugin update --all regelmäßig ausführen oder im Admin-Panel prüfen.