Skip to content

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ÉtiquetteNotes
WordPresswordpress:7.0.2-php8.5-apacheou 7.0.2-php8.5-fpm-alpine
PHP8.5inclus dans l'image WordPress
MariaDBmariadb:12.3lts pointe actuellement vers 12.3.2
Redisredis:8.10-alpineserveur seul, sans persistance
nginxnginx:1.30.4-alpineseulement si vous le lancez en conteneur
Redis Object Cache2.8.0extension, embarque Predis 2.4.0
phpredis6.3.0optionnel, voir l'étape 3
WP-CLIwordpress:cli-php8.5side-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             :6379

La 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 containers

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

bash
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-content
bash
mkdir -p config backup data/db data/wordpress

Pas de répertoire de données pour Redis. Plus de détails à l'étape 4.

Étape 2 : .env

bash
# 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=wp

Générez les mots de passe plutôt que de les taper :

bash
openssl rand -base64 24

Ensuite 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 fallback

Utilisez 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é) :

dockerfile
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/pear

FPM sur Alpine (les outils de compilation ne sont pas préinstallés, on les ajoute puis on les retire) :

dockerfile
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/pear

Remplacez 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

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 = 60

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

yaml
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/html

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

bash
sudo apt install nginx certbot python3-certbot-nginx
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;
    }
}
bash
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.com

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

yaml
  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:ro

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

nginx
# /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 :

yaml
  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 already

Avec 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

bash
docker compose up -d
docker compose logs -f wordpress

N'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.php

Vé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 :

bash
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

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

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

bash
docker compose run --rm wpcli plugin install redis-cache --activate
docker compose run --rm wpcli redis enable
docker compose run --rm wpcli redis status

redis status vous dit quel client il a retenu :

Status:      Connected
Client:      Predis (v2.4.0)
Drop-in:     Valid

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

bash
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 --> render

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

bash
#!/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 :

bash
gunzip -c backup/db-20260802-1130.sql.gz | \
  docker compose exec -T db mariadb -u root -p"$MARIADB_ROOT_PASSWORD" wordpress

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

bash
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 --all

Les 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ômeCause 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ôterien n'écoute sur 127.0.0.1:8080. Vérifiez docker compose ps et ss -ltnp | grep 8080
"Primary script unknown", voie BSCRIPT_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 Bnginx 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 infiniemême cause que ci-dessus
La limite d'envoi affiche toujours 2Mini 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 routeclient_max_body_size dans nginx est inférieur à post_max_size dans PHP
Écran blanc après activation du cache objetwp-content/object-cache.php périmé. Supprimez-le, puis relancez wpcli redis enable
Extension active, mais rien dans Redisle 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 Predisle conteneur tourne encore sur l'ancienne image. docker compose up -d --force-recreate wordpress
L'installation d'extensions réclame des identifiants FTPFS_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éec'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.