WordPress mit Docker selbst hosten: Apache oder PHP-FPM, plus ein Redis-Objektcache
Jedes WordPress-auf-Docker-Tutorial hört bei "es startet" auf. Dieses geht weiter: gepinnte Versionen, ein echter Objektcache mit der kompilierten PHP-Erweiterung, ein Reverse Proxy, der TLS wirklich terminiert, und die zwei oder drei Eigenheiten des offiziellen Images, die Sie an Tag 30 einholen und nicht an Tag eins.
Die Versionen unten waren aktuell, als ich am 2. August 2026 nachgesehen habe. Pinnen Sie sie. Liefern Sie kein :latest aus.
| Komponente | Tag | Hinweise |
|---|---|---|
| WordPress | wordpress:7.0.2-php8.5-apache | oder 7.0.2-php8.5-fpm-alpine |
| PHP | 8.5 | im WordPress-Image enthalten |
| MariaDB | mariadb:12.3 | lts löst derzeit auf 12.3.2 auf |
| Redis | redis:8.10-alpine | nur Server, keine Persistenz |
| nginx | nginx:1.30.4-alpine | nur wenn Sie ihn als Container betreiben |
| Redis Object Cache | 2.8.0 | Plugin, bringt Predis 2.4.0 mit |
| phpredis | 6.3.0 | optional, siehe Schritt 3 |
| WP-CLI | wordpress:cli-php8.5 | Sidecar, bei Bedarf ausführen |
WordPress 7.1 erscheint am 19. August 2026, rechnen Sie also damit, dass sich der Teil 7.0.2 dieses Tags bald ändert.
Der Stack
Drei Container erledigen die Arbeit. Von außen ist nichts erreichbar außer dem einen Port, den Sie veröffentlichen, und den veröffentlichen Sie nur auf Loopback.
[ nginx, terminating TLS ]
|
v
127.0.0.1:8080
|
===========|=================== docker
v
[ wp_app ] wordpress + php
|
| service names on wp_default
+--------+--------+
| |
[ wp_db ] [ wp_redis ]
:3306 :6379Datenbank und Redis veröffentlichen nie einen Port. Sie sind aus den anderen Containern über den Dienstnamen erreichbar, db und redis, und sonst von nirgendwo.
Zwei Entscheidungen
Wie PHP läuft. Apache mit mod_php in einem Container, oder PHP-FPM. Apache ist ein Image, ein Prozess, keine zusätzliche Konfigurationsdatei. FPM gibt Ihnen einen eigenen Webserver, der statische Dateien ausliefern kann, ohne PHP anzufassen, und kostet Sie eine nginx-Konfiguration, die Ihnen ab jetzt gehört.
Wo nginx läuft. Auf dem Host, oder als vierter Container.
edge = nginx on the host edge = nginx in compose
A) apache image publish 127.0.0.1:8080 proxy_pass wordpress:80
proxy_pass to it 4 containers
3 containers <-- start here
B) fpm image publish 127.0.0.1:9000 fastcgi_pass wordpress:9000
fastcgi_pass to it 4 containers
3 containersBeginnen Sie mit A auf einem nginx des Hosts. Das sind die wenigsten beweglichen Teile, und certbot auf dem Host erledigt die Erneuerungen, ohne dass Sie daran denken müssen. Wechseln Sie zu B, wenn Sie Caching und Rate Limiting pro Location wollen, oder wenn Sie so viele statische Dateien ausliefern, dass es sich lohnt, sie aus PHP herauszuhalten.
Die Schritte 1 bis 5 sind für jede Kombination identisch.
Voraussetzungen
Docker Engine 27 oder neuer mit dem Compose-v2-Plugin. Das ist die ganze Liste.
Legen Sie das Projekt dorthin, wo nginx hindurchkommt. Wenn der nginx des Hosts ./data/wordpress direkt liest (Weg B), funktioniert /root/wp nicht, egal was Sie chmodden, denn /root ist 700. Nehmen Sie /srv/wp oder /opt/wp.
sudo mkdir -p /srv/wp && cd /srv/wpSchritt 1: Aufbau
/srv/wp/
├─ .env # secrets and sizing
├─ compose.yaml
├─ Dockerfile # adds phpredis
├─ config/
│ ├─ php.ini # PHP + opcache overrides
│ └─ nginx.conf # only if nginx runs as a container
├─ backup/
└─ data/
├─ db/ # MariaDB datadir
└─ wordpress/ # core + wp-contentmkdir -p config backup data/db data/wordpressKein Datenverzeichnis für Redis. Mehr dazu in Schritt 4.
Schritt 2: .env
# Database
MARIADB_ROOT_PASSWORD=change_me_root
MARIADB_DATABASE=wordpress
MARIADB_USER=wordpress
MARIADB_PASSWORD=change_me_user
WORDPRESS_TABLE_PREFIX=wp_
# Where the stack listens on the host (loopback only)
HTTP_BIND=127.0.0.1:8080
# WordPress memory
WP_MEMORY_LIMIT=256M
WP_MAX_MEMORY_LIMIT=512M
# Redis
REDIS_MAXMEMORY=256mb
WP_REDIS_PREFIX=wpErzeugen Sie die Passwörter, statt sie zu tippen:
openssl rand -base64 24Danach chmod 600 .env und ab damit in die .gitignore.
WP_REDIS_PREFIX wird in dem Moment wichtig, in dem Sie eine zweite WordPress-Seite betreiben. Zwei Seiten, die sich ein Redis ohne Präfix teilen, lesen gegenseitig ihre Cache-Schlüssel, und die Symptome wirken wie ein Spuk.
Schritt 3: einen Redis-Client wählen
Diesen Schritt können Sie überspringen. Es gibt ihn, weil PHP auf zwei Wegen mit Redis sprechen kann und viele Anleitungen den schwierigeren als Pflicht darstellen.
Das offizielle WordPress-Image liefert bcmath, exif, gd, intl, mysqli, zip und imagick mit. Die Redis-Erweiterung liefert es nicht mit. Das macht nichts, denn das Plugin Redis Object Cache bringt Predis mit, einen in reinem PHP geschriebenen Redis-Client, und nutzt ihn automatisch, wenn die Erweiterung fehlt.
phpredis (PECL) Predis (bundled)
client language C extension PHP
in stock image no, must build yes, ships with plugin
custom Dockerfile required not needed
rebuild on every yes no
upstream update
plugin support yes yes, the default fallbackNehmen Sie das Standard-Image. Bringen Sie den Cache zum Laufen, öffnen Sie dann den Metrics-Tab des Plugins und schauen Sie sich die gemeldeten Millisekunden pro Request an. Wenn diese Zahl Sie stört, kommen Sie zurück und kompilieren Sie die Erweiterung. Auf einer Visitenkartenseite werden Sie den Unterschied nicht merken. Auf einem WooCommerce mit vierzig Plugins und mehreren hundert wp_cache_get-Aufrufen pro Seite schon.
Die Kosten des eigenen Images sind nicht das Dockerfile, sondern dass Sie ab jetzt jedes Mal neu bauen, wenn Upstream einen Sicherheitsfix für WordPress oder PHP veröffentlicht. Wissen Sie, was Sie kaufen.
Wenn Sie es wollen, legen Sie ein Dockerfile neben compose.yaml.
Apache (Debian-Basis, PHPIZE_DEPS bereits installiert):
FROM wordpress:7.0.2-php8.5-apache
RUN set -eux; \
pecl install redis-6.3.0; \
docker-php-ext-enable redis; \
rm -rf /tmp/pearFPM auf Alpine (Build-Werkzeuge sind nicht vorinstalliert, also hinzufügen und wieder entfernen):
FROM wordpress:7.0.2-php8.5-fpm-alpine
RUN set -eux; \
apk add --no-cache --virtual .build-deps $PHPIZE_DEPS; \
pecl install redis-6.3.0; \
docker-php-ext-enable redis; \
apk del .build-deps; \
rm -rf /tmp/pearTauschen Sie dann im nächsten Schritt image: gegen build:. Das Plugin erkennt die Erweiterung von selbst; Sie können die Wahl auch festlegen mit define('WP_REDIS_CLIENT', 'phpredis'); oder 'predis'.
Schritt 4: config/php.ini
upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
max_execution_time = 300
max_input_time = 300
max_input_vars = 3000
; the base image sets 4000 files and revalidate_freq=2,
; both too tight for a plugin-heavy production site
opcache.memory_consumption = 192
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.revalidate_freq = 60Ein verbreitetes Muster ist, diese Werte als Umgebungsvariablen hineinzureichen und sie durch Überschreiben des command im Container in eine ini-Datei zu schreiben. Lassen Sie es. command zu überschreiben heißt, dass Sie ab jetzt einen Shell-Einzeiler pflegen, der auf docker-entrypoint.sh apache2-foreground enden muss, und ein Tippfehler darin kostet Sie den Entrypoint, der WordPress auspackt und wp-config.php schreibt. Eine eingehängte Datei ist eine Zeile YAML und übersteht jedes Image-Upgrade.
Das Präfix zz- am Einhängeziel sorgt dafür, dass PHP sie zuletzt lädt, sodass sie sich gegen die bildeigene opcache-recommended.ini durchsetzt.
Schritt 5: compose.yaml
Das ist Weg A: Apache, auf Loopback veröffentlicht, damit ein nginx des Hosts als Proxy davor sitzt.
name: wp
services:
db:
image: mariadb:12.3
container_name: wp_db
restart: unless-stopped
environment:
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:?missing in .env}
MARIADB_DATABASE: ${MARIADB_DATABASE:-wordpress}
MARIADB_USER: ${MARIADB_USER:-wordpress}
MARIADB_PASSWORD: ${MARIADB_PASSWORD:?missing in .env}
MARIADB_AUTO_UPGRADE: "1"
volumes:
- ./data/db:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 12
start_period: 60s
redis:
image: redis:8.10-alpine
container_name: wp_redis
restart: unless-stopped
command: >
redis-server
--save ""
--appendonly no
--maxmemory ${REDIS_MAXMEMORY:-256mb}
--maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
wordpress:
image: wordpress:7.0.2-php8.5-apache
# only if you compiled phpredis in step 3:
# build:
# context: .
# dockerfile: Dockerfile
# image: wp-wordpress:local
container_name: wp_app
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "${HTTP_BIND:-127.0.0.1:8080}:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: ${MARIADB_DATABASE:-wordpress}
WORDPRESS_DB_USER: ${MARIADB_USER:-wordpress}
WORDPRESS_DB_PASSWORD: ${MARIADB_PASSWORD:?missing in .env}
WORDPRESS_TABLE_PREFIX: ${WORDPRESS_TABLE_PREFIX:-wp_}
WORDPRESS_CONFIG_EXTRA: |
define('WP_REDIS_HOST', 'redis');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_PREFIX', '${WP_REDIS_PREFIX:-wp}');
define('WP_REDIS_DATABASE', 0);
define('WP_REDIS_TIMEOUT', 1);
define('WP_REDIS_READ_TIMEOUT', 1);
define('WP_CACHE', true);
define('WP_MEMORY_LIMIT', '${WP_MEMORY_LIMIT:-256M}');
define('WP_MAX_MEMORY_LIMIT', '${WP_MAX_MEMORY_LIMIT:-512M}');
define('DISALLOW_FILE_EDIT', true);
define('FS_METHOD', 'direct');
volumes:
- ./data/wordpress:/var/www/html
- ./config/php.ini:/usr/local/etc/php/conf.d/zz-wordpress.ini:ro
wpcli:
image: wordpress:cli-php8.5
profiles: [tools]
user: "33:33"
depends_on:
db:
condition: service_healthy
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: ${MARIADB_DATABASE:-wordpress}
WORDPRESS_DB_USER: ${MARIADB_USER:-wordpress}
WORDPRESS_DB_PASSWORD: ${MARIADB_PASSWORD:?missing in .env}
WORDPRESS_TABLE_PREFIX: ${WORDPRESS_TABLE_PREFIX:-wp_}
volumes:
- ./data/wordpress:/var/www/htmlKein networks:-Block. Compose legt jeden Dienst in ein Projektnetz namens wp_default und gibt jedem einen DNS-Namen, der dem Dienstnamen entspricht, weshalb WORDPRESS_DB_HOST gleich db:3306 ist. Netze von Hand zu deklarieren bringt hier nichts.
Fünf Dinge, die Erwähnung verdienen.
Die Bind-Adresse ist nicht optional. "8080:80" zu schreiben veröffentlicht auf jeder Schnittstelle, und Docker schiebt seine eigenen iptables-Regeln vor Ihre, sodass ufw munter meldet, Port 8080 sei blockiert, während das halbe Internet Ihr WordPress über einfaches HTTP liest. 127.0.0.1:8080:80 ist die Lösung.
${VAR:?message} bricht docker compose up mit Ihrer Meldung ab, wenn eine Variable fehlt. Ohne das beschert Ihnen ein Tippfehler in .env eine Datenbank mit leerem Passwort.
Redis hat kein Volume, --save "", --appendonly no. Ein Objektcache ist abgeleitete Information. Ihn zu persistieren bringt Ihnen einen warmen Cache nach einem Neustart und kostet Sie auf Dauer fork-basierte Schreibzugriffe. Ein kalter Cache füllt sich in Sekunden wieder. Wenn Sie Redis später für Sitzungen oder eine Warteschlange nutzen, ändert sich diese Rechnung.
--maxmemory-policy allkeys-lru ist die wichtige Option. Redis nutzt standardmäßig noeviction, was bedeutet: sobald der Cache voll ist, schlagen Schreibvorgänge fehl und WordPress wirft Cache-Fehler, statt still die kältesten Schlüssel zu verdrängen.
Die Healthchecks laufen alle 10s, nicht alle 120s. depends_on: service_healthy wartet auf eine bestandene Prüfung, ein langsames Intervall wird also unmittelbar zu einem langsamen Start. Zwei Minuten, in denen der Stack kaputt aussieht, sind keine Tugend.
Falls Sie einen volumes:-Block auf oberster Ebene mitgeschleppt haben, der db_data und Konsorten deklariert, löschen Sie ihn. Diese benannten Volumes bleiben ungenutzt, wenn Sie ./data/db per Bind-Mount einbinden.
Schritt 6: nginx auf dem Host
Installieren Sie nginx und certbot und legen Sie dann einen vhost an:
sudo apt install nginx certbot python3-certbot-nginx# /etc/nginx/sites-available/example.com
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
client_max_body_size 64m; # keep in sync with post_max_size
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_read_timeout 300s;
}
}sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d example.com -d www.example.comX-Forwarded-Proto ist der Header, auf den es ankommt. WordPress entscheidet anhand dessen, ob es http://- oder https://-URLs ausgibt, und die wp-config des offiziellen Images liest ihn bereits (siehe Schritt 8). Lassen Sie den Header weg, bekommen Sie Mixed-Content-Warnungen oder eine Weiterleitungsschleife.
Dass client_max_body_size und post_max_size nicht zusammenpassen, ist die mit Abstand häufigste Ursache für "mein 30-MB-Video-Upload stirbt bei 90%". nginx weist den Request-Body ab, bevor PHP ihn überhaupt sieht, das PHP-Limit allein anzuheben bringt Sie also nicht weiter.
Certbot schreibt die Datei um, fügt einen 443-Block und eine Weiterleitung hinzu und installiert einen Erneuerungs-Timer. Prüfen Sie ihn mit systemctl list-timers | grep certbot.
Schritt 6, Weg B: PHP-FPM
Zwei Änderungen an der Compose-Datei. Richten Sie den Build auf das FPM-Dockerfile und veröffentlichen Sie FastCGI statt HTTP:
wordpress:
image: wordpress:7.0.2-php8.5-fpm-alpine
# or build: from the fpm-alpine Dockerfile in step 3
container_name: wp_app
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "127.0.0.1:9000:9000" # FastCGI, loopback only, no exceptions
environment:
# unchanged from route A
...
volumes:
- ./data/wordpress:/var/www/html
- ./config/php.ini:/usr/local/etc/php/conf.d/zz-wordpress.ini:roPort 9000 spricht FastCGI, nicht HTTP. FastCGI hat weder Authentifizierung noch TLS, und alles, was ihn erreicht, kann beliebiges PHP ausführen. Wenn Sie sich je dabei ertappen, 9000:9000 ohne das Loopback-Präfix zu tippen, halten Sie inne.
Jetzt liefert der nginx des Hosts die statischen Dateien selbst aus und reicht nur PHP an den Container weiter:
# /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com;
root /srv/wp/data/wordpress; # the HOST path
index index.php;
client_max_body_size 64m;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
try_files $uri =404;
include fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
# the container sees these files at /var/www/html, not at the host path
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT /var/www/html;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_read_timeout 300;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
}
location ~* \.(js|css|png|jpe?g|gif|ico|svg|webp|avif|woff2?)$ {
expires 30d;
add_header Cache-Control "public";
access_log off;
try_files $uri =404;
}
# never execute PHP that arrived through the media uploader
location ^~ /wp-content/uploads/ {
location ~ \.php$ { deny all; }
}
location = /favicon.ico { log_not_found off; access_log off; }
location = /robots.txt { log_not_found off; access_log off; allow all; }
location ~ /\. { deny all; }
location = /xmlrpc.php { deny all; } # drop this line if you use the WP mobile app
}Diese beiden fastcgi_param-Überschreibungen sind der ganze Trick, und sie zu überspringen ist der Grund, warum dieser Aufbau meist aufgegeben wird. nginx berechnet SCRIPT_FILENAME aus seinem eigenen root, nämlich /srv/wp/data/wordpress. PHP-FPM sucht diesen Pfad dann im Container, wo es ihn nicht gibt, und antwortet mit "Primary script unknown" oder einer weißen Seite ohne brauchbaren Eintrag im nginx-Log. Die beiden Dateisysteme sind dieselben Dateien unter verschiedenen Pfaden, und nur Sie kennen die Zuordnung.
Zwei kleinere Anmerkungen zu diesem Weg. try_files $uri =404 innerhalb der PHP-Location schließt das Path-Info-Loch zur Codeausführung, das Standardkonfigurationen von 2013 noch mit sich tragen. Und die HTTPS-Erkennung funktioniert hier anders: es gibt keinen Reverse Proxy, also auch kein X-Forwarded-Proto. Debians fastcgi_params übergibt HTTPS=on direkt, sobald certbot den TLS-Block eingerichtet hat, und WordPress greift das von selbst auf.
Schritt 6, Alternative: nginx als Container
Nehmen Sie das, wenn der Host kein nginx hat und Sie lieber alles in Docker halten. Fügen Sie einen vierten Dienst hinzu und veröffentlichen Sie keinen Port mehr aus wordpress:
nginx:
image: nginx:1.30.4-alpine
container_name: wp_nginx
restart: unless-stopped
depends_on: [wordpress]
ports:
- "80:80"
- "443:443"
volumes:
- ./data/wordpress:/var/www/html:ro # only needed for the fpm variant
- ./config/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./config/certs:/etc/nginx/certs:ro # if you have certs alreadyBeim Apache-Image ist config/nginx.conf die Proxy-Konfiguration aus Schritt 6, nur mit proxy_pass http://wordpress:80; statt 127.0.0.1:8080. Beim FPM-Image ist es die FastCGI-Konfiguration mit fastcgi_pass wordpress:9000;, root /var/www/html; und ohne Pfadumschreibung, weil sich beide Container innerhalb von Docker über den Pfad einig sind.
Seien Sie beim Thema TLS ehrlich zu sich. Ein nginx im Container ergibt Sinn, wenn Sie bereits Zertifikate auf der Platte haben, oder wenn die Seite intern ist und über einfaches HTTP läuft, oder wenn weiter vorn etwas TLS für Sie terminiert. Automatische Let's-Encrypt-Erneuerung in diesem Container zum Laufen zu bringen heißt: ein certbot-Sidecar, ein gemeinsamer Webroot und ein Reload-Hook. Wenn das kein Projekt ist, das Sie wollen, nehmen Sie den nginx des Hosts, oder tauschen Sie diesen Dienst gegen Caddy oder Traefik, die ACME beide selbst erledigen.
Schritt 7: erster Start
docker compose up -d
docker compose logs -f wordpressFühren Sie docker compose build nur dann zuerst aus, wenn Sie sich in Schritt 3 für das eigene Image entschieden haben.
Sie achten auf zwei Zeilen:
WordPress not found in /var/www/html - copying now...
No 'wp-config.php' found in /var/www/html, but 'WORDPRESS_...' variables supplied; copying ...So ungefähr läuft der Start ab:
t=0s db starting health: starting
t~20s db healthy healthcheck.sh --connect passes
t~20s redis healthy
t~21s wordpress starts unpack core -> generate wp-config.phpPrüfen Sie, ob der Stack auf Loopback antwortet, bevor Sie nginx ins Spiel bringen. Das ist der schnellste Weg herauszufinden, welche Hälfte kaputt ist:
curl -I http://127.0.0.1:8080 # route A
# HTTP/1.1 302 Found
# Location: http://127.0.0.1:8080/wp-admin/install.phpÖffnen Sie danach die Domain und schließen Sie die Fünf-Minuten-Installation ab.
Schritt 8: was das Image in wp-config.php schreibt
Drei Dinge, die man wissen sollte, bevor man sie braucht.
Die Salts werden einmal erzeugt. Die Auth-Keys und Salts stammen aus /dev/urandom in dem Moment, in dem die Datei angelegt wird, und werden wörtlich hineingeschrieben. Sie werden nicht bei jedem Start aus der Umgebung gelesen. Wenn Sie sie selbst verwalten wollen, setzen Sie WORDPRESS_AUTH_KEY, WORDPRESS_SECURE_AUTH_KEY, WORDPRESS_LOGGED_IN_KEY, WORDPRESS_NONCE_KEY, WORDPRESS_AUTH_SALT, WORDPRESS_SECURE_AUTH_SALT, WORDPRESS_LOGGED_IN_SALT und WORDPRESS_NONCE_SALT vor dem ersten Start. Sie später zu ändern loggt alle aus, was nach einem Einbruch genau das ist, was Sie wollen.
Alles andere wird live gelesen. WORDPRESS_CONFIG_EXTRA und die WORDPRESS_DB_*-Variablen laufen bei jedem Request durch einen Helfer getenv_docker(), und der Extra-Block geht durch eval(). WORDPRESS_CONFIG_EXTRA zu bearbeiten und docker compose up -d auszuführen wirkt daher sofort. Sie müssen wp-config.php nicht löschen.
Die Reverse-Proxy-Korrektur ist schon drin. Sie müssen sie nicht ergänzen:
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false) {
$_SERVER['HTTPS'] = 'on';
}Wenn Sie trotzdem eine endlose HTTPS-Weiterleitung bekommen, liegt das Problem weiter vorn: nginx sendet den Header nicht.
Schritt 9: den Objektcache einschalten
Drei Befehle, auf dem Standard-Image:
docker compose run --rm wpcli plugin install redis-cache --activate
docker compose run --rm wpcli redis enable
docker compose run --rm wpcli redis statusredis status sagt Ihnen, welchen Client es gewählt hat:
Status: Connected
Client: Predis (v2.4.0)
Drop-in: ValidClient: PhpRedis (v6.3.0) heißt, die Erweiterung ist vorhanden und im Einsatz. Beide Zeilen bedeuten einen funktionierenden Cache. Bestätigen Sie die Erweiterung separat mit docker compose exec wordpress php -m | grep -x redis, was auf dem Standard-Image erwartungsgemäß nichts ausgibt.
redis enable legt wp-content/object-cache.php an seinen Platz. Diese Datei ist der ganze Mechanismus. Ohne sie tun WP_CACHE und die WP_REDIS_*-Konstanten überhaupt nichts. Das Plugin zu installieren und zu aktivieren genügt für sich allein nicht; das Drop-in muss existieren, weshalb ein fehlgeschlagener Schreibvorgang in wp-content sich auch als Cache zeigt, der still nichts tut.
Die uid im Dienst wpcli ist keine Deko. Das CLI-Image basiert auf Alpine, wo www-data die uid 82 hat. Das WordPress-Image mit Apache basiert auf Debian, wo www-data die uid 33 hat. Führen Sie WP-CLI mit der falschen uid aus, wird jede Datei, die es schreibt, für PHP unlesbar. Passen Sie es an Ihr Basis-Image an:
wordpress:...-apache Debian www-data = 33 -> user: "33:33"
wordpress:...-fpm Debian www-data = 33 -> user: "33:33"
wordpress:...-fpm-alpine Alpine www-data = 82 -> user: "82:82"Prüfen Sie, ob Schlüssel in Redis ankommen, laden Sie die Seite dann ein paar Mal und sehen Sie den Zahlen beim Steigen zu:
docker compose exec redis redis-cli info keyspace
# db0:keys=412,expires=7,avg_ttl=0
docker compose exec redis redis-cli info stats | grep keyspace
# keyspace_hits:18342
# keyspace_misses:611So sieht der Weg eines Requests aus, wenn es läuft:
request
|
v
[ nginx ] -> [ apache | php-fpm ]
|
v
[ PHP: wp_cache_get ]
|
+-- HIT --> render from Redis, ~2 SQL queries
|
+-- MISS --> MariaDB --> wp_cache_set --> renderEine Trefferquote unter etwa 90% nach einem Tag Verkehr bedeutet meist ein Verdrängungsproblem, kein WordPress-Problem. Prüfen Sie, ob used_memory an maxmemory klebt, und erhöhen Sie REDIS_MAXMEMORY.
Ein Objektcache ist kein Seitencache. Er nimmt wiederholte Datenbankabfragen aus jedem Request heraus, auch aus Requests angemeldeter Nutzer und aus dem Adminbereich. Er hält WordPress nicht davon ab, PHP zu starten. Vollseiten-Caching gehört in nginx oder ein Plugin und ist eine eigene Entscheidung.
Schritt 10: Sicherungen
Packen Sie kein laufendes MariaDB-Datenverzeichnis mit tar ein. In ./data/db wird geschrieben, während das Archiv entsteht, und die Kopie, die Sie zurückbekommen, ist womöglich nicht konsistent. Exportieren Sie die Datenbank und archivieren Sie die Dateien getrennt davon.
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
source .env
stamp=$(date +%Y%m%d-%H%M)
docker compose exec -T db \
mariadb-dump --single-transaction --quick --no-tablespaces \
-u root -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE" \
| gzip > "backup/db-$stamp.sql.gz"
tar -czf "backup/files-$stamp.tar.gz" -C data/wordpress wp-content
find backup -name '*.gz' -mtime +14 -delete--single-transaction liefert Ihnen unter InnoDB einen konsistenten Snapshot, ohne Schreiber zu sperren. Ab MariaDB 11 heißt das Programm mariadb-dump; mysqldump existiert noch als Symlink, aber nicht für immer.
Wiederherstellen:
gunzip -c backup/db-20260802-1130.sql.gz | \
docker compose exec -T db mariadb -u root -p"$MARIADB_ROOT_PASSWORD" wordpressNur wp-content lohnt die Archivierung. Die Core-Dateien kommen aus dem Image, und wp-config.php lässt sich aus Ihrer Compose-Datei reproduzieren, abgesehen von diesen Salts. Sichern Sie die Salts ein einziges Mal.
Schritt 11: aktualisieren
Der Punkt, der die Leute überrascht. Sehen Sie sich die Absicherung im Entrypoint an:
if [ ! -e index.php ] && [ ! -e wp-includes/version.php ]; then
echo >&2 "WordPress not found in $PWD - copying now..."Core-Dateien werden nur dann aus dem Image kopiert, wenn das Verzeichnis leer ist. Sobald ./data/wordpress eine Installation enthält, bringt Ihnen ein Pull von wordpress:7.1-php8.5-apache den neueren PHP-Build, den neueren Apache und die neueren Erweiterungen, und lässt Ihre WordPress-Core-Dateien genau dort, wo sie waren.
Aktualisierungen zerfallen also in zwei Teile:
Image tag -> PHP, Apache/FPM, extensions, base OS
docker compose pull && docker compose up -d
(add `docker compose build --pull` if you use the custom image)
WP core -> dashboard, or:
docker compose run --rm wpcli core update
docker compose run --rm wpcli core update-db
docker compose run --rm wpcli plugin update --allGrößere MariaDB-Sprünge brauchen MARIADB_AUTO_UPGRADE: "1", das in der Compose-Datei oben bereits steht. Es führt mariadb-upgrade aus, wenn es ein älteres Datenverzeichnis erkennt. Ziehen Sie trotzdem vorher einen Dump, und gehen Sie eine Hauptversion nach der anderen.
Fehlersuche
| Symptom | Wahrscheinliche Ursache |
|---|---|
| "Error establishing a database connection" beim ersten Start | ./data/db wurde in einem früheren Lauf mit einem anderen Passwort initialisiert. Das MariaDB-Image wendet Zugangsdaten nur auf ein leeres Datenverzeichnis an. Löschen Sie es oder setzen Sie das Passwort von Hand zurück |
| 502 Bad Gateway vom nginx des Hosts | auf 127.0.0.1:8080 lauscht nichts. Prüfen Sie docker compose ps und ss -ltnp | grep 8080 |
| "Primary script unknown", Weg B | SCRIPT_FILENAME zeigt weiterhin auf den Host-Pfad. Überschreiben Sie es mit /var/www/html$fastcgi_script_name |
| 403 von nginx bei statischen Dateien, Weg B | nginx kommt nicht bis ./data/wordpress durch. Holen Sie das Projekt aus einem übergeordneten Verzeichnis mit 700 heraus |
Seite lädt, aber jede URL ist http:// | nginx setzt X-Forwarded-Proto nicht |
| Endlose HTTPS-Weiterleitung | dieselbe Ursache wie oben |
| Upload-Limit zeigt weiterhin 2M | ini nicht eingehängt, oder außerhalb von /usr/local/etc/php/conf.d/ eingehängt. Prüfen Sie docker compose exec wordpress php -i | grep upload_max |
| Upload bricht auf halbem Weg ab | client_max_body_size in nginx ist kleiner als post_max_size in PHP |
| Weiße Seite nach dem Einschalten des Objektcaches | veraltete wp-content/object-cache.php. Löschen Sie sie und dann erneut wpcli redis enable |
| Plugin aktiv, aber nichts in Redis | das Drop-in wurde nie geschrieben. redis status meldet, das Drop-in sei ungültig. Prüfen Sie, ob wp-content beschreibbar ist |
redis status meldet "Not connected" | falscher WP_REDIS_HOST, oder die Container liegen in verschiedenen Compose-Projekten |
Eigenes Image gebaut, aber redis status sagt weiterhin Predis | der Container läuft noch mit dem alten Image. docker compose up -d --force-recreate wordpress |
| Plugin-Installation fragt nach FTP-Zugangsdaten | FS_METHOD steht nicht auf direct, oder der PHP-Benutzer darf wp-content nicht beschreiben |
| Neues Image geholt, WordPress-Version unverändert | so erwartet. Aktualisieren Sie den Core über WP-CLI oder das Dashboard |
| Cache-Trefferquote hängt bei etwa 60% | Redis ist an maxmemory und verdrängt. Erhöhen Sie REDIS_MAXMEMORY |
Was als Nächstes zu tun ist
Legen Sie das Sicherungsskript aus Schritt 10 auf einen Cron-Eintrag und testen Sie die Wiederherstellung in ein Wegwerfverzeichnis, bevor Sie sie brauchen. Messen Sie dann: installieren Sie Query Monitor, notieren Sie die Zahl der Abfragen auf Ihrer langsamsten Seite mit abgeschaltetem Objektcache, schalten Sie ihn ein und sehen Sie noch einmal nach. Diese Zahl ist der einzige Beweis, dass irgendetwas davon gewirkt hat.
Danach lohnt es sich vor allem, die Lücke zu schließen und zu merken, dass es der Maschine schlecht geht, bevor ein Besucher es Ihnen sagt. Prometheus, Node Exporter und Grafana richtet genau das auf derselben Art einzelnem Server ein, und schon das Node-Exporter-Dashboard sagt Ihnen, ob Redis wirklich richtig dimensioniert ist.
Selbst hosten ist das meiste von dem, was ich tue: über 30 Open-Source-Plattformen ausgerollt und gepflegt, für Kunden im Lauf des letzten Jahrzehnts. Wenn Sie das lieber nicht selbst betreiben möchten, melden Sie sich.