WordPress – Betriebs­dokumentation

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.

WordPress
6.9.x (wordpress:6.9-fpm)
Betriebssystem
AlmaLinux 9
Kubernetes
K3s (Single-Node)
Datenbank
MariaDB 10.11 (K3s Pod)
Webserver
nginx 1.30 (Sidecar)
Ingress
nginx-ingress F5 2.5.1
Design-Entscheidung: Single-Node, alles in K3s Anders als bei Nextcloud läuft hier auch die Datenbank (MariaDB) als K3s-Pod. Die Daten liegen auf einem HostPath-Volume (/srv/wordpress-db). Es gibt keine Hochverfügbarkeit. Updates verursachen eine kurze Ausfallzeit (typisch: 30–60 Sekunden).

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: wp_login 5r/s, xmlrpc.php → 403 │ └──────────────────────────┬──────────────────────────┘ │ HTTP www.apt-upgrade.me ▼ ┌──────────────────────────────────────────┐ │ Pod: wordpress (Namespace: wordpress) │ │ ┌───────────────────────────────────┐ │ │ │ nginx sidecar :80 │ │ │ │ statische Dateien + PHP-Proxy │ │ │ └─────────────┬─────────────────────┘ │ │ fastcgi │ :9000 │ │ ┌─────────────▼─────────────────────┐ │ │ │ wordpress-fpm PHP-FPM 8.2 │ │ │ │ wordpress:6.9-fpm │ │ │ └───────────────────────────────────┘ │ └──────────────────────────────────────────┘ │ │ TCP :3306 ▼ ┌──────────────────────────┐ │ Pod: mariadb │ │ mariadb:10.11 │ │ Namespace: wordpress │ └──────────────────────────┘┌───────────────────────────────────────────────┐ │ HostPath Volumes │ │ /srv/wordpress-www → /var/www/html │ │ /srv/wordpress-db → /var/lib/mysql │ └───────────────────────────────────────────────┘ ┌──────────────────────────────────────────┐ │ K3s CronJob: wordpress-cron (alle 5 min)│ │ php /var/www/html/wp-cron.php │ └──────────────────────────────────────────┘

Warum dieser Ansatz?

KomponenteWo 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.

Warum sind WordPress-FPM und nginx im selben Pod? Beide Container teilen denselben Netzwerk-Namespace und die gleichen Volumes. Dadurch kann nginx über 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 HostMountpoint im ContainerInhalt
/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)
Wichtig: wp-content/uploads enthält Mediendateien Bilder, PDFs und andere Uploads liegen in /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?

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

BereichZweckRelevant 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)TypWertZweck
www.apt-upgrade.me A Öffentliche IP des Servers (217.154.101.78) WordPress-Blog, Let's Encrypt TLS
IPv6 (optional) Falls der Server eine öffentliche IPv6-Adresse hat, kann zusätzlich ein AAAA-Record angelegt werden. Die Firewall-Regeln (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ßnahmeAnnotation / KonfigurationWert
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
Rate-Limit-Zonen im Controller definiert Die Zone 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

DateiInhalt
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)

RolleAufgabe
blog_packagesPakete installieren (z.B. SELinux-Tools)
blog_selinuxSELinux enforcing, container_file_t für HostPath-Verzeichnisse
common_k3sK3s, Helm, cert-manager, nginx-ingress (F5) – gemeinsame Basis
blog_k3s_deployK8s-Manifeste: Namespace, Secrets, ConfigMaps, Deployments, Ingress, CronJob
blog_wordpressPost-Deploy: WP-CLI-Befehle (Admin, Plugins, Einstellungen)

Templates (erzeugen die K8s-Manifeste)

TemplateErzeugtWichtig 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-FormatBedeutungBeispiel
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

KomponenteURL
WordPresshttps://hub.docker.com/_/wordpress/tags (Filter: *-fpm)
MariaDBhttps://hub.docker.com/_/mariadb/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
Patch-Update ohne Tag-Änderung Wenn Docker Hub z.B. 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 WordPress hat einen eingebauten Pseudo-Cron, der bei jedem Seitenaufruf geprüft wird. In diesem Setup ist er deaktiviert (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)

  1. SSH-Port auf 22 setzen in inventory/host_vars/www.apt-upgrade.me/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 blog.yml --limit www.apt-upgrade.me --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 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.

Grafana
https://www.apt-upgrade.me/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:30104 (NodePort)

Sidecar im MariaDB-Pod; Prometheus scrapt via NodePort 30104
Prometheus-Job: mysqld_wordpress

php-fpm_exporter
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-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 – 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).

PanelWas 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).

PanelWas 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.
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 – Konfigurationshinweis

mysqld_exporter v0.19.0: kein DATA_SOURCE_NAME mehr Ab v0.19.0 wird 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.

Warum kein System-Cron auf dem Host? WordPress läuft im Container. Der Host hat keinen Zugriff auf 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:

  1. ssh -p 10022 root@217.154.101.78
    dnf upgrade -y
  2. Prüfen ob ein Reboot nötig ist:
    needs-restarting -r
  3. 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:

  1. Neues Image manuell pullen:
    crictl pull docker.io/library/wordpress:6.9-fpm
  2. 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)

Backup vor jedem Major-Update! Immer zuerst die Datenbank und wp-content sichern, bevor der Tag geändert wird.
  1. Tag in inventory/group_vars/all.yml ändern:
    blog_image_wordpress: "wordpress:7.0-fpm"
  2. 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
  3. 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

MariaDB 10.11 ist LTS (Support bis Juni 2028) Ein Upgrade auf 11.x oder 12.x erfordert eine Datenbank-Migration und ist kein einfacher Tag-Wechsel. Nicht ohne Test-Lauf durchführen.

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

WasPfad (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
MariaDB läuft als K3s-Pod – nicht auf dem Host Anders als bei Nextcloud läuft MariaDB hier im WordPress-Pod. Der Dump wird per 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

  1. Server neu aufsetzen (Infrastructure):
    ansible-playbook blog.yml --limit www.apt-upgrade.me --ask-vault-pass
  2. Restore-Skript ausführen:
    cd ~/www_k3s
    ./scripts/wordpress-restore.sh

    Ablauf: 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

  3. Ansible erneut ausführen (stellt Plugin-Liste und WP-Config sicher):
    ansible-playbook blog.yml --limit www.apt-upgrade.me --ask-vault-pass
MariaDB-Daten-Dir beim Restore leeren Das Restore-Skript löscht /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:

MariaDB-Pod startet nicht

k3s kubectl logs deployment/mariadb -n wordpress --previous

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

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 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.

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/www.apt-upgrade.me/vault.yml
WordPress-Plugins regelmäßig aktualisieren Veraltete Plugins sind die häufigste Einfallsroute für WordPress-Angriffe. wp plugin update --all regelmäßig ausführen oder im Admin-Panel prüfen.
WP-Admin-Benutzer absichern Starkes Passwort verwenden. Zwei-Faktor-Authentifizierung empfohlen (z.B. via Plugin "Two Factor"). Das Admin-Passwort liegt in vault.yml.