Auto-héberger WordPress avec Docker : Apache ou PHP-FPM, plus un cache objet Redis
Tous les tutoriels WordPress sur Docker s'arrêtent à "ça démarre". Celui-ci va plus loin : versions épinglées, un vrai cache objet avec l'extension PHP compilée, un proxy inverse qui termine réellement TLS, et les deux ou trois comportements de l'image officielle qui vous rattraperont au trentième jour plutôt qu'au premier.
Les versions ci-dessous étaient à jour quand j'ai vérifié, le 2 août 2026. Épinglez-les. Ne mettez pas :latest en production.
| Composant | Étiquette | Notes |
|---|---|---|
| WordPress | wordpress:7.0.2-php8.5-apache | ou 7.0.2-php8.5-fpm-alpine |
| PHP | 8.5 | inclus dans l'image WordPress |
| MariaDB | mariadb:12.3 | lts pointe actuellement vers 12.3.2 |
| Redis | redis:8.10-alpine | serveur seul, sans persistance |
| nginx | nginx:1.30.4-alpine | seulement si vous le lancez en conteneur |
| Redis Object Cache | 2.8.0 | extension, embarque Predis 2.4.0 |
| phpredis | 6.3.0 | optionnel, voir l'étape 3 |
| WP-CLI | wordpress:cli-php8.5 | side-car, lancé à la demande |
WordPress 7.1 sort le 19 août 2026, attendez-vous donc à ce que la partie 7.0.2 de cette étiquette bouge bientôt.
La pile
Trois conteneurs font le travail. Rien n'est joignable de l'extérieur en dehors de l'unique port que vous publiez, et vous le publiez uniquement sur la boucle locale.
[ 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 :6379La base de données et Redis ne publient jamais de port. Ils sont joignables depuis les autres conteneurs par nom de service, db et redis, et de nulle part ailleurs.
Deux décisions
Comment PHP s'exécute. Apache avec mod_php dans un conteneur, ou PHP-FPM. Apache, c'est une image, un processus, aucun fichier de configuration supplémentaire. FPM vous donne un serveur web distinct capable de servir les fichiers statiques sans toucher à PHP, et vous coûte une configuration nginx dont vous devenez responsable.
Où nginx s'exécute. Sur l'hôte, ou comme quatrième conteneur.
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 containersCommencez par A avec un nginx sur l'hôte. C'est le montage avec le moins de pièces mobiles, et certbot sur l'hôte gère les renouvellements sans que vous y pensiez. Passez à B si vous voulez du cache et de la limitation de débit par emplacement, ou si vous servez assez de fichiers statiques pour qu'il vaille la peine de les tenir à l'écart de PHP.
Les étapes 1 à 5 sont identiques pour toutes les combinaisons.
Prérequis
Docker Engine 27 ou plus récent avec le greffon Compose v2. C'est toute la liste.
Placez le projet là où nginx peut passer. Si le nginx de l'hôte lit ./data/wordpress directement (voie B), /root/wp ne fonctionnera pas quels que soient vos chmod, car /root est en 700. Utilisez /srv/wp ou /opt/wp.
sudo mkdir -p /srv/wp && cd /srv/wpÉtape 1 : arborescence
/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/wordpressPas de répertoire de données pour Redis. Plus de détails à l'étape 4.
Étape 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=wpGénérez les mots de passe plutôt que de les taper :
openssl rand -base64 24Ensuite chmod 600 .env et ajoutez-le au .gitignore.
WP_REDIS_PREFIX devient important dès que vous faites tourner un deuxième site WordPress. Deux sites partageant un même Redis sans préfixe liront les clés de cache l'un de l'autre, et les symptômes ressemblent à une maison hantée.
Étape 3 : choisir un client Redis
Vous pouvez sauter cette étape. Elle existe parce qu'il y a deux façons pour PHP de parler à Redis, et beaucoup de guides présentent la plus difficile comme obligatoire.
L'image officielle de WordPress fournit bcmath, exif, gd, intl, mysqli, zip et imagick. Elle ne fournit pas l'extension Redis. Cela n'a pas d'importance, car l'extension Redis Object Cache embarque Predis, un client Redis écrit en PHP pur, et l'utilise automatiquement quand l'extension est absente.
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 fallbackUtilisez l'image telle quelle. Faites d'abord fonctionner le cache, puis ouvrez l'onglet Metrics de l'extension et regardez les millisecondes qu'elle rapporte par requête. Si ce chiffre vous dérange, revenez compiler l'extension. Sur un site vitrine, vous ne verrez pas la différence. Sur un WooCommerce avec quarante extensions et plusieurs centaines d'appels à wp_cache_get par page, si.
Le coût de l'image sur mesure, ce n'est pas le Dockerfile, c'est que vous reconstruisez désormais à chaque correctif de sécurité WordPress ou PHP publié en amont. Sachez ce que vous achetez.
Si vous la voulez, ajoutez un Dockerfile à côté de compose.yaml.
Apache (base Debian, PHPIZE_DEPS déjà installé) :
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 sur Alpine (les outils de compilation ne sont pas préinstallés, on les ajoute puis on les retire) :
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/pearRemplacez ensuite image: par build: à l'étape suivante. L'extension détecte le module toute seule ; vous pouvez aussi figer le choix avec define('WP_REDIS_CLIENT', 'phpredis'); ou 'predis'.
Étape 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 = 60Un schéma répandu consiste à passer ces valeurs en variables d'environnement et à les réécrire dans un fichier ini en surchargeant le command du conteneur. Laissez tomber. Surcharger command signifie que vous maintenez désormais une ligne de shell qui doit se terminer par docker-entrypoint.sh apache2-foreground, et une faute de frappe là vous coûte le point d'entrée qui décompresse WordPress et écrit wp-config.php. Un fichier monté, c'est une ligne de YAML, et cela survit à chaque mise à jour d'image.
Le préfixe zz- sur la cible de montage fait que PHP le charge en dernier, il l'emporte donc sur le opcache-recommended.ini de l'image.
Étape 5 : compose.yaml
Voici la voie A : Apache, publié sur la boucle locale pour qu'un nginx de l'hôte serve de proxy.
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/htmlPas de bloc networks:. Compose place chaque service sur un réseau de projet appelé wp_default et donne à chacun un nom DNS correspondant à son nom de service, d'où WORDPRESS_DB_HOST valant db:3306. Déclarer les réseaux à la main n'apporte rien ici.
Cinq points méritent d'être signalés.
L'adresse d'écoute n'est pas optionnelle. Écrire "8080:80" publie sur toutes les interfaces, et Docker insère ses propres règles iptables devant les vôtres, si bien qu'ufw vous annoncera tranquillement que le port 8080 est bloqué pendant que tout internet lit votre WordPress en HTTP en clair. 127.0.0.1:8080:80 est le correctif.
${VAR:?message} interrompt docker compose up avec votre message quand une variable manque. Sans cela, une faute de frappe dans .env vous donne une base de données au mot de passe vide.
Redis n'a pas de volume, --save "", --appendonly no. Un cache objet est une donnée dérivée. Le persister vous achète un cache chaud après un redémarrage et vous coûte des écritures disque par fork pour toujours. Un cache froid se remplit en quelques secondes. Si vous utilisez plus tard Redis pour des sessions ou une file d'attente, ce calcul change.
--maxmemory-policy allkeys-lru est l'option importante. Redis utilise noeviction par défaut, ce qui veut dire qu'une fois le cache plein, les écritures échouent et WordPress se met à lancer des erreurs de cache au lieu d'évincer discrètement les clés les plus froides.
Les healthchecks tournent toutes les 10 s, pas toutes les 120 s. depends_on: service_healthy attend une vérification réussie, un intervalle lent se traduit donc directement par un démarrage lent. Deux minutes pendant lesquelles la pile paraît cassée, ce n'est pas une vertu.
Si vous avez repris un bloc volumes: de premier niveau déclarant db_data et consorts, supprimez-le. Ces volumes nommés sont inutilisés quand vous montez ./data/db en bind mount.
Étape 6 : nginx sur l'hôte
Installez nginx et certbot, puis déposez un vhost :
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 est l'en-tête qui compte. WordPress décide d'émettre des URL en http:// ou en https:// d'après lui, et le wp-config de l'image officielle le lit déjà (voir l'étape 8). Supprimez cet en-tête et vous obtenez des avertissements de contenu mixte, ou une boucle de redirection.
Un désaccord entre client_max_body_size et post_max_size est de loin la cause la plus fréquente de "mon envoi de vidéo de 30 Mo meurt à 90%". nginx rejette le corps de la requête avant même que PHP le voie, augmenter la seule limite PHP ne mène donc nulle part.
Certbot réécrit le fichier pour ajouter un bloc 443 et une redirection, et installe une minuterie de renouvellement. Vérifiez-la avec systemctl list-timers | grep certbot.
Étape 6, voie B : PHP-FPM
Deux changements dans le fichier compose. Pointez la construction vers le Dockerfile FPM, et publiez FastCGI au lieu de 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:roLe port 9000 parle FastCGI, pas HTTP. FastCGI n'a ni authentification ni TLS, et tout ce qui peut l'atteindre peut exécuter du PHP arbitraire. Si vous vous surprenez un jour à taper 9000:9000 sans le préfixe de boucle locale, arrêtez-vous.
Maintenant le nginx de l'hôte sert lui-même les fichiers statiques et ne passe que le PHP au conteneur :
# /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
}Ces deux surcharges de fastcgi_param constituent toute l'astuce, et les omettre est la raison pour laquelle ce montage finit généralement abandonné. nginx calcule SCRIPT_FILENAME à partir de son propre root, qui vaut /srv/wp/data/wordpress. PHP-FPM cherche alors ce chemin dans le conteneur, où il n'existe pas, et renvoie "Primary script unknown" ou une page blanche sans rien d'utile dans le journal nginx. Les deux systèmes de fichiers sont les mêmes fichiers à des chemins différents, et vous seul connaissez la correspondance.
Deux remarques mineures sur cette voie. try_files $uri =404 à l'intérieur du bloc PHP referme la faille d'exécution de code par path-info que les configurations standard de 2013 traînent encore. Et la détection HTTPS fonctionne différemment ici : il n'y a pas de proxy inverse, donc pas de X-Forwarded-Proto. Le fastcgi_params de Debian transmet HTTPS=on directement une fois que certbot a mis en place le bloc TLS, et WordPress le récupère tout seul.
Étape 6, variante : nginx en conteneur
Utilisez ceci si l'hôte n'a pas de nginx et que vous préférez tout garder dans Docker. Ajoutez un quatrième service et cessez de publier un port depuis 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 alreadyAvec l'image Apache, config/nginx.conf est la configuration de proxy de l'étape 6 avec proxy_pass http://wordpress:80; à la place de 127.0.0.1:8080. Avec l'image FPM, c'est la configuration FastCGI avec fastcgi_pass wordpress:9000;, root /var/www/html;, et sans remappage de chemin, car à l'intérieur de Docker les deux conteneurs s'accordent sur le chemin.
Soyez lucide sur TLS ici. Un nginx en conteneur a du sens quand vous avez déjà des certificats sur le disque, ou quand le site est interne et tourne en HTTP simple, ou quand quelque chose en amont termine TLS pour vous. Faire fonctionner le renouvellement automatique Let's Encrypt dans ce conteneur suppose un side-car certbot, un webroot partagé et un hook de rechargement. Si ce n'est pas un chantier qui vous tente, utilisez le nginx de l'hôte, ou remplacez ce service par Caddy ou Traefik, qui gèrent tous deux ACME d'eux-mêmes.
Étape 7 : premier démarrage
docker compose up -d
docker compose logs -f wordpressN'exécutez docker compose build en premier que si vous avez choisi l'image sur mesure à l'étape 3.
Vous guettez deux lignes :
WordPress not found in /var/www/html - copying now...
No 'wp-config.php' found in /var/www/html, but 'WORDPRESS_...' variables supplied; copying ...En gros, voici l'enchaînement du démarrage :
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.phpVérifiez que la pile répond sur la boucle locale avant d'impliquer nginx. C'est le moyen le plus rapide de savoir laquelle des deux moitiés est cassée :
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.phpOuvrez ensuite le domaine et terminez l'installation de cinq minutes.
Étape 8 : ce que l'image écrit dans wp-config.php
Trois faits qu'il vaut mieux connaître avant d'en avoir besoin.
Les salts sont générés une seule fois. Les clés d'authentification et les salts proviennent de /dev/urandom au moment où le fichier est créé, et y sont écrits littéralement. Ils ne sont pas relus depuis l'environnement à chaque démarrage. Pour les gérer vous-même, définissez 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 et WORDPRESS_NONCE_SALT avant le premier démarrage. Les changer plus tard déconnecte tout le monde, ce qui est exactement ce que vous voulez après une intrusion.
Tout le reste est lu en direct. WORDPRESS_CONFIG_EXTRA et les variables WORDPRESS_DB_* passent par un utilitaire getenv_docker() à chaque requête, et le bloc supplémentaire est exécuté par eval(). Modifier WORDPRESS_CONFIG_EXTRA puis lancer docker compose up -d prend donc effet immédiatement. Vous n'avez pas besoin de supprimer wp-config.php.
Le correctif pour le proxy inverse est déjà là. Inutile de l'ajouter :
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false) {
$_SERVER['HTTPS'] = 'on';
}Si vous obtenez malgré tout une redirection HTTPS infinie, le problème est en amont : nginx n'envoie pas l'en-tête.
Étape 9 : activer le cache objet
Trois commandes, sur l'image telle quelle :
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 vous dit quel client il a retenu :
Status: Connected
Client: Predis (v2.4.0)
Drop-in: ValidClient: PhpRedis (v6.3.0) signifie que l'extension est présente et utilisée. L'une comme l'autre de ces lignes correspond à un cache qui fonctionne. Confirmez l'extension séparément avec docker compose exec wordpress php -m | grep -x redis, qui n'affiche rien sur l'image telle quelle, comme attendu.
redis enable met wp-content/object-cache.php en place. Ce fichier est tout le mécanisme. Sans lui, WP_CACHE et les constantes WP_REDIS_* ne font absolument rien. Installer et activer l'extension ne suffit pas à soi seul ; le drop-in doit exister, ce qui explique aussi qu'une écriture ratée dans wp-content se manifeste par un cache qui ne fait silencieusement rien.
L'uid du service wpcli n'est pas décorative. L'image CLI est basée sur Alpine, où www-data porte l'uid 82. L'image WordPress avec Apache est basée sur Debian, où www-data porte l'uid 33. Lancez WP-CLI avec la mauvaise uid et chaque fichier qu'il écrit devient illisible pour PHP. Alignez-la sur votre image de base :
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"Vérifiez que des clés arrivent bien dans Redis, puis chargez le site quelques fois et regardez les chiffres grimper :
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:611À quoi ressemble le trajet d'une requête une fois que cela fonctionne :
request
|
v
[ nginx ] -> [ apache | php-fpm ]
|
v
[ PHP: wp_cache_get ]
|
+-- HIT --> render from Redis, ~2 SQL queries
|
+-- MISS --> MariaDB --> wp_cache_set --> renderUn taux de succès sous les 90% environ après une journée de trafic signale généralement un problème d'éviction, pas un problème WordPress. Regardez si used_memory s'est collé à maxmemory et augmentez REDIS_MAXMEMORY.
Un cache objet n'est pas un cache de pages. Il retire les requêtes de base de données répétées de chaque requête, y compris celles des utilisateurs connectés et de l'administration. Il n'empêche pas WordPress de démarrer PHP. La mise en cache de pages entières relève de nginx ou d'une extension, et c'est une décision distincte.
Étape 10 : sauvegardes
N'archivez pas avec tar un répertoire de données MariaDB en fonctionnement. ./data/db est en cours d'écriture pendant la création de l'archive, et la copie que vous récupérez peut être cohérente ou non. Exportez la base de données et archivez les fichiers séparément.
#!/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 vous donne un instantané cohérent sur InnoDB sans bloquer les écrivains. Sous MariaDB 11 et suivantes, le binaire s'appelle mariadb-dump ; mysqldump existe encore comme lien symbolique, mais pas pour toujours.
Restauration :
gunzip -c backup/db-20260802-1130.sql.gz | \
docker compose exec -T db mariadb -u root -p"$MARIADB_ROOT_PASSWORD" wordpressSeul wp-content mérite d'être archivé. Les fichiers du cœur viennent de l'image, et wp-config.php est reproductible à partir de votre fichier compose, à l'exception de ces salts. Sauvegardez les salts une fois.
Étape 11 : mettre à jour
Le point qui surprend. Regardez la garde du point d'entrée :
if [ ! -e index.php ] && [ ! -e wp-includes/version.php ]; then
echo >&2 "WordPress not found in $PWD - copying now..."Les fichiers du cœur ne sont copiés depuis l'image que lorsque le répertoire est vide. Dès que ./data/wordpress contient une installation, récupérer wordpress:7.1-php8.5-apache vous donne la nouvelle version de PHP, le nouvel Apache et les nouvelles extensions, et laisse vos fichiers du cœur WordPress exactement où ils étaient.
Les mises à jour se scindent donc en deux :
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 --allLes versions majeures de MariaDB réclament MARIADB_AUTO_UPGRADE: "1", déjà présent dans le fichier compose ci-dessus. Il lance mariadb-upgrade quand il détecte un répertoire de données plus ancien. Faites tout de même un export au préalable, et avancez d'une version majeure à la fois.
Dépannage
| Symptôme | Cause probable |
|---|---|
| "Error establishing a database connection" au premier démarrage | ./data/db a été initialisé avec un autre mot de passe lors d'une exécution antérieure. L'image MariaDB n'applique les identifiants qu'à un répertoire de données vide. Effacez-le ou réinitialisez le mot de passe à la main |
| 502 Bad Gateway depuis le nginx de l'hôte | rien n'écoute sur 127.0.0.1:8080. Vérifiez docker compose ps et ss -ltnp | grep 8080 |
| "Primary script unknown", voie B | SCRIPT_FILENAME pointe encore sur le chemin de l'hôte. Surchargez-le en /var/www/html$fastcgi_script_name |
| 403 de nginx sur les fichiers statiques, voie B | nginx ne peut pas atteindre ./data/wordpress. Sortez le projet d'un répertoire parent en 700 |
Le site charge mais toutes les URL sont en http:// | nginx ne positionne pas X-Forwarded-Proto |
| Redirection HTTPS infinie | même cause que ci-dessus |
| La limite d'envoi affiche toujours 2M | ini non monté, ou monté hors de /usr/local/etc/php/conf.d/. Vérifiez docker compose exec wordpress php -i | grep upload_max |
| L'envoi meurt en cours de route | client_max_body_size dans nginx est inférieur à post_max_size dans PHP |
| Écran blanc après activation du cache objet | wp-content/object-cache.php périmé. Supprimez-le, puis relancez wpcli redis enable |
| Extension active, mais rien dans Redis | le drop-in n'a jamais été écrit. redis status indiquera que le drop-in n'est pas valide. Vérifiez que wp-content est accessible en écriture |
redis status affiche "Not connected" | mauvais WP_REDIS_HOST, ou les conteneurs sont dans des projets Compose différents |
Image sur mesure construite mais redis status dit toujours Predis | le conteneur tourne encore sur l'ancienne image. docker compose up -d --force-recreate wordpress |
| L'installation d'extensions réclame des identifiants FTP | FS_METHOD n'est pas sur direct, ou l'utilisateur PHP ne peut pas écrire dans wp-content |
| Nouvelle image récupérée, version de WordPress inchangée | c'est attendu. Mettez à jour le cœur via WP-CLI ou le tableau de bord |
| Taux de succès du cache bloqué autour de 60% | Redis est à maxmemory et évince. Augmentez REDIS_MAXMEMORY |
Que faire ensuite
Mettez le script de sauvegarde de l'étape 10 dans une entrée cron et testez la restauration dans un répertoire jetable avant d'en avoir besoin. Mesurez ensuite : installez Query Monitor, notez le nombre de requêtes de votre page la plus lente avec le cache objet désactivé, activez-le, et regardez à nouveau. Ce chiffre est la seule preuve que tout cela a servi.
Ensuite, la lacune qui mérite d'être comblée, c'est de savoir que la machine va mal avant qu'un visiteur ne vous le dise. Prometheus, Node Exporter et Grafana met cela en place sur le même genre de serveur unique, et le seul tableau de bord Node Exporter vous dira si Redis est vraiment bien dimensionné.
L'auto-hébergement représente l'essentiel de mon activité : plus de 30 plateformes open source déployées et maintenues pour des clients au cours de la dernière décennie. Si vous préférez ne pas gérer cela vous-même, écrivez-moi.