Si vous avez déjà perdu une heure parce qu’un plugin ne reproduit le bug que sur “la machine du collègue”, le problème n’est pas WordPress : c’est l’environnement. Un setup Docker bien pensé vous donne un WordPress 6.9.4 reproductible, versionné, et jetable.

Le besoin / Le problème serveur

Vous devez développer et déboguer WordPress 6.9.4 (avril 2026) avec PHP 8.1+ sans dépendre d’un MAMP/XAMPP local fragile, ni d’un serveur mutualisé. Vous voulez aussi :

  • un Nginx “proche prod” (réécritures, cache, headers) ;
  • une base MariaDB contrôlée (charset/collation, import/export propres) ;
  • WP-CLI disponible partout (install, search-replace, exports) ;
  • un mode debug avec Xdebug (mais désactivé par défaut) ;
  • un cache serveur optionnel (Redis) pour tester des bugs de cache ;
  • un piège classique évité : “ça marche chez moi” parce que les versions PHP/extensions diffèrent.

À la fin, vous aurez un projet Docker Compose complet, prêt à cloner, qui installe WordPress 6.9.4, configure Nginx/PHP-FPM/MariaDB, et vous donne des commandes de staging/restauration via WP-CLI.

Résumé rapide

  • Docker Compose orchestre 5 services : nginx, php-fpm, mariadb, redis (optionnel), mailpit (optionnel).
  • WordPress est monté via volume dans /var/www/html pour éditer le code en local.
  • Une image PHP dédiée installe les extensions usuelles (gd, intl, zip, imagick si souhaité) + WP-CLI.
  • Xdebug est activable via variable d’environnement, sans pénaliser le runtime par défaut.
  • Des scripts WP-CLI automatisent install, reset DB, import/export, search-replace (staging).
  • Vous testez avec curl, wp core version, et des checks SQL (charset, tables).

Avant de commencer (prérequis)

Accès et outils :

  • SSH/terminal local (Linux/macOS) ou PowerShell (Windows) ;
  • Docker Desktop ou Docker Engine + Compose v2 ;
  • Un éditeur (VS Code, PhpStorm) ;
  • Optionnel mais pratique : mkcert pour TLS local, ou un proxy type Traefik/Caddy.

Sauvegarde obligatoire (même en dev). J’ai souvent vu des développeurs “tester un import” et écraser leur DB de staging sans dump.

  • Export DB avant manip : wp db export (dans le conteneur WP-CLI) ou mysqldump.

Versions ciblées :

  • WordPress : 6.9.4 (stable installée) ;
  • PHP : 8.1+ (je recommande 8.3 en dev si vos plugins/thèmes suivent, mais je garde 8.2/8.3 “switchables”) ;
  • DB : MariaDB 10.11 LTS (ou MySQL 8.0/8.4 si vous collez à prod) ;
  • Serveur : Nginx stable.

Sources officielles utiles :

Étape 1 : Structurer le projet Docker (Compose, volumes, secrets)

Arborescence que j’utilise en équipe. Le point clé : séparer infra (docker/) et app (wp/) pour éviter de polluer le dépôt.

mkdir -p wp docker/nginx docker/php docker/scripts docker/mysql
touch docker-compose.yml .env

Créez un .env minimal. Évitez de committer des mots de passe réels ; en dev, ce n’est pas “grave”, mais c’est un mauvais pli.

cat > .env <<'EOF'
# Domaine local (ajoutez-le dans /etc/hosts si besoin)
APP_HOST=wp.local
APP_PORT_HTTP=8080

# Versions
WP_VERSION=6.9.4
PHP_VERSION=8.2

# Base de données
DB_NAME=wordpress
DB_USER=wp
DB_PASSWORD=wp
DB_ROOT_PASSWORD=root
DB_HOST=db

# WordPress
WP_TABLE_PREFIX=wp_
WP_DEBUG=1

# Xdebug (0/1)
XDEBUG_ENABLE=0

# Redis (0/1)
REDIS_ENABLE=1
EOF

docker-compose.yml : un Compose “lisible”, avec healthchecks et dépendances. Notez la stratégie : Nginx dépend du healthcheck PHP, et WP-CLI dépend de DB+PHP.

cat > docker-compose.yml <<'EOF'
name: wp-dev-694

services:
  nginx:
    image: nginx:1.27-alpine
    depends_on:
      php:
        condition: service_healthy
    ports:
      - "${APP_PORT_HTTP}:80"
    volumes:
      - ./wp:/var/www/html:delegated
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    networks: [wpnet]

  php:
    build:
      context: ./docker/php
      args:
        PHP_VERSION: "${PHP_VERSION}"
    environment:
      # Variables WordPress (utilisées par wp-config.php)
      WORDPRESS_DB_HOST: "${DB_HOST}"
      WORDPRESS_DB_NAME: "${DB_NAME}"
      WORDPRESS_DB_USER: "${DB_USER}"
      WORDPRESS_DB_PASSWORD: "${DB_PASSWORD}"
      WORDPRESS_TABLE_PREFIX: "${WP_TABLE_PREFIX}"
      WORDPRESS_DEBUG: "${WP_DEBUG}"
      # Xdebug toggle
      XDEBUG_ENABLE: "${XDEBUG_ENABLE}"
      # Redis toggle
      REDIS_ENABLE: "${REDIS_ENABLE}"
    volumes:
      - ./wp:/var/www/html:delegated
      - ./docker/php/php.ini:/usr/local/etc/php/conf.d/99-custom.ini:ro
    healthcheck:
      test: ["CMD-SHELL", "php -v >/dev/null 2>&1 && php -m | grep -q 'mysqli'"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks: [wpnet]

  db:
    image: mariadb:10.11
    environment:
      MARIADB_DATABASE: "${DB_NAME}"
      MARIADB_USER: "${DB_USER}"
      MARIADB_PASSWORD: "${DB_PASSWORD}"
      MARIADB_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
    command:
      # Charset/collation cohérents avec WordPress (utf8mb4)
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      # Logs utiles en dev (attention au bruit)
      - --log_error_verbosity=3
      - --max_connections=200
    volumes:
      - dbdata:/var/lib/mysql
      - ./docker/mysql/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -uroot -p${DB_ROOT_PASSWORD} --silent"]
      interval: 10s
      timeout: 5s
      retries: 20
    networks: [wpnet]

  redis:
    image: redis:7.4-alpine
    profiles: ["redis"]
    command: ["redis-server", "--appendonly", "no"]
    networks: [wpnet]

  mailpit:
    image: axllent/mailpit:v1.21
    profiles: ["mail"]
    ports:
      - "8025:8025"
    networks: [wpnet]

  wpcli:
    build:
      context: ./docker/php
      args:
        PHP_VERSION: "${PHP_VERSION}"
    depends_on:
      db:
        condition: service_healthy
      php:
        condition: service_healthy
    entrypoint: ["/bin/sh", "/usr/local/bin/wp-entrypoint.sh"]
    environment:
      WORDPRESS_DB_HOST: "${DB_HOST}"
      WORDPRESS_DB_NAME: "${DB_NAME}"
      WORDPRESS_DB_USER: "${DB_USER}"
      WORDPRESS_DB_PASSWORD: "${DB_PASSWORD}"
      WORDPRESS_TABLE_PREFIX: "${WP_TABLE_PREFIX}"
      WORDPRESS_DEBUG: "${WP_DEBUG}"
      WP_VERSION: "${WP_VERSION}"
      APP_HOST: "${APP_HOST}"
    volumes:
      - ./wp:/var/www/html
      - ./docker/scripts/wp-entrypoint.sh:/usr/local/bin/wp-entrypoint.sh:ro
    networks: [wpnet]

networks:
  wpnet:

volumes:
  dbdata:
EOF

Deux profils optionnels : redis et mail. Vous les activez avec --profile quand vous en avez besoin, au lieu de tout faire tourner en permanence.

Étape 2 : Construire l’image PHP-FPM (extensions, Xdebug, outils)

Je préfère construire une image PHP plutôt que d’utiliser wordpress:php-fpm en dev avancé, car vous contrôlez précisément les extensions et l’outillage (WP-CLI, composer, git, xdebug). C’est aussi là que les équipes se plantent : une extension manquante (intl, zip) et vous chasez un “bug” qui n’en est pas un.

Dockerfile PHP-FPM, compatible PHP 8.1+ via build arg. On installe les extensions courantes pour WordPress 6.9.4, et on prépare Xdebug (activable via env).

cat > docker/php/Dockerfile <<'EOF'
ARG PHP_VERSION=8.2
FROM php:${PHP_VERSION}-fpm-alpine

# Paquets système
RUN apk add --no-cache 
    bash curl git unzip 
    icu-dev oniguruma-dev 
    libzip-dev zlib-dev 
    libpng-dev libjpeg-turbo-dev freetype-dev 
    imagemagick imagemagick-dev 
    mariadb-client 
    $PHPIZE_DEPS

# Extensions PHP nécessaires
RUN docker-php-ext-configure gd --with-freetype --with-jpeg 
  && docker-php-ext-install -j$(nproc) 
    mysqli pdo_mysql 
    intl mbstring 
    zip exif 
    opcache 
  && pecl install imagick 
  && docker-php-ext-enable imagick

# Xdebug (installé mais activé via ini conditionnel)
RUN pecl install xdebug 
  && docker-php-ext-enable xdebug

# WP-CLI (phar officiel)
RUN curl -sSLo /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar 
  && chmod +x /usr/local/bin/wp 
  && wp --info

# Utilisateur non-root pour limiter les dégâts (dev ≠ no-limit)
RUN addgroup -g 1000 -S www 
  && adduser -u 1000 -S www -G www

WORKDIR /var/www/html

# Script d'entrypoint commun (php-fpm)
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

HEALTHCHECK --interval=10s --timeout=5s --retries=10 CMD php -v >/dev/null 2>&1 || exit 1

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["php-fpm", "-F"]
EOF

entrypoint.sh : active/désactive Xdebug proprement au runtime. Le piège classique : laisser Xdebug activé et conclure que “WordPress est lent”.

cat > docker/php/entrypoint.sh <<'EOF'
#!/bin/sh
set -eu

# Activation conditionnelle de Xdebug pour éviter de ralentir tout le monde
if [ "${XDEBUG_ENABLE:-0}" = "1" ]; then
  cat > /usr/local/etc/php/conf.d/20-xdebug.ini </dev/null || true
fi

# Permissions de base (attention : en équipe, adaptez UID/GID)
chown -R www:www /var/www/html 2>/dev/null || true

exec "$@"
EOF

php.ini custom : opcache activé (même en dev, utile pour se rapprocher de prod), limites cohérentes, et logs d’erreurs visibles.

cat > docker/php/php.ini <<'EOF'
; Réglages PHP pour WordPress 6.9.4+ (dev)
date.timezone=UTC
memory_limit=512M
upload_max_filesize=128M
post_max_size=128M
max_execution_time=120

display_errors=1
display_startup_errors=1
log_errors=1
error_reporting=E_ALL

; OPcache (utile pour reproduire des bugs liés au cache opcode)
opcache.enable=1
opcache.validate_timestamps=1
opcache.revalidate_freq=0
opcache.max_accelerated_files=20000
opcache.memory_consumption=192
opcache.interned_strings_buffer=16
EOF

Étape 3 : Configurer Nginx (WordPress, multisite optionnel, uploads)

Nginx en reverse vers PHP-FPM, avec une config WordPress standard. Je garde la config explicite (pas de include magique), parce que c’est là que vous déboguez les 404 de permaliens et les soucis d’assets.

cat > docker/nginx/default.conf <<'EOF'
server {
  listen 80;
  server_name _;

  root /var/www/html;
  index index.php index.html;

  # Logs (dev)
  access_log /var/log/nginx/access.log;
  error_log  /var/log/nginx/error.log warn;

  # Sécurité basique (dev, ajustez en prod)
  add_header X-Content-Type-Options nosniff always;
  add_header X-Frame-Options SAMEORIGIN always;
  add_header Referrer-Policy strict-origin-when-cross-origin always;

  # WordPress : permaliens
  location / {
    try_files $uri $uri/ /index.php?$args;
  }

  # PHP
  location ~ .php$ {
    try_files $uri =404;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param HTTPS off;
    fastcgi_pass php:9000;

    # Buffers (réduit les erreurs 502 sur réponses lourdes)
    fastcgi_buffers 16 16k;
    fastcgi_buffer_size 32k;
  }

  # Bloquer l'accès à certains fichiers
  location ~* /(wp-config.php|readme.html|license.txt) {
    deny all;
  }

  # Cache statique simple (dev)
  location ~* .(css|js|jpg|jpeg|png|gif|ico|svg|webp)$ {
    expires 7d;
    add_header Cache-Control "public, max-age=604800";
    try_files $uri =404;
  }
}
EOF

Multisite : si vous en avez besoin, vous adapterez try_files et les règles spécifiques. La doc officielle Nginx WordPress est un bon point de départ : developer.wordpress.org.

Étape 4 : Base de données (MariaDB), charset, perf, import/export

WordPress 6.9.4 tourne très bien sur MariaDB 10.11 LTS. Ce qui casse le plus souvent en migration/staging, c’est :

  • un dump importé avec un mauvais charset (accent/emoji cassés) ;
  • des collations mixtes ;
  • un search-replace fait “à la main” qui corrompt la sérialisation PHP.

Je vous conseille de vérifier le charset côté serveur et côté tables. Commandes utiles :

# Démarrage de base
docker compose up -d --build

# Vérifier le charset serveur
docker compose exec -T db mariadb -uroot -p"${DB_ROOT_PASSWORD}" -e "SHOW VARIABLES LIKE 'character_set_server'; SHOW VARIABLES LIKE 'collation_server';"

# Vérifier la collation des tables
docker compose exec -T db mariadb -uroot -p"${DB_ROOT_PASSWORD}" -e "SELECT TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA='${DB_NAME}' LIMIT 20;"

Import/export propre en SQL :

# Export (dump)
docker compose exec -T db mariadb-dump -uroot -p"${DB_ROOT_PASSWORD}" "${DB_NAME}" > ./docker/mysql/backup.sql

# Import (attention : écrasez d'abord si nécessaire)
cat ./docker/mysql/backup.sql | docker compose exec -T db mariadb -uroot -p"${DB_ROOT_PASSWORD}" "${DB_NAME}"

Étape 5 : WP-CLI, bootstrap WordPress 6.9.4, staging et resets

WP-CLI est votre couteau suisse. Le piège que je vois le plus : lancer wp depuis le mauvais conteneur (nginx) ou depuis le host sans les bons binaires PHP/extensions. Ici, le service wpcli utilise la même image que php, donc mêmes extensions, même version PHP.

Créez l’entrypoint WP-CLI qui :

  • télécharge WordPress 6.9.4 si absent ;
  • génère un wp-config.php (si absent) ;
  • installe le site ;
  • fait un check DB ;
  • laisse la main pour exécuter d’autres commandes.
cat > docker/scripts/wp-entrypoint.sh <<'EOF'
#!/bin/sh
set -eu

cd /var/www/html

# Astuce : WP-CLI doit tourner en tant que www pour éviter des fichiers root-owned
# (sinon vous vous battez avec les permissions Git/IDE)
if id www >/dev/null 2>&1; then
  WP_USER="www"
else
  WP_USER="$(id -u)"
fi

# Télécharger WordPress si absent
if [ ! -f wp-settings.php ]; then
  echo "Téléchargement de WordPress ${WP_VERSION}..."
  wp core download --version="${WP_VERSION}" --locale=fr_FR --allow-root
fi

# Générer wp-config.php si absent
if [ ! -f wp-config.php ]; then
  echo "Génération de wp-config.php..."
  wp config create 
    --dbname="${WORDPRESS_DB_NAME}" 
    --dbuser="${WORDPRESS_DB_USER}" 
    --dbpass="${WORDPRESS_DB_PASSWORD}" 
    --dbhost="${WORDPRESS_DB_HOST}" 
    --dbprefix="${WORDPRESS_TABLE_PREFIX}" 
    --skip-check 
    --allow-root

  # Activer debug en dev (attention à ne pas pousser ça en prod)
  wp config set WP_DEBUG "${WORDPRESS_DEBUG}" --raw --allow-root
  wp config set WP_DEBUG_LOG true --raw --allow-root
  wp config set WP_DEBUG_DISPLAY true --raw --allow-root

  # URL forcée (utile quand vous changez de port)
  wp config set WP_HOME "http://${APP_HOST}" --allow-root
  wp config set WP_SITEURL "http://${APP_HOST}" --allow-root
fi

# Attendre que la DB réponde
echo "Attente de la base..."
until wp db check --allow-root >/dev/null 2>&1; do
  sleep 2
done

# Installer WordPress si non installé
if ! wp core is-installed --allow-root >/dev/null 2>&1; then
  echo "Installation WordPress..."
  wp core install 
    --url="http://${APP_HOST}" 
    --title="WP Docker Dev" 
    --admin_user="admin" 
    --admin_password="admin" 
    --admin_email="[email protected]" 
    --skip-email 
    --allow-root

  # Permaliens (évite le classique “404 partout”)
  wp rewrite structure '/%postname%/' --hard --allow-root
  wp rewrite flush --hard --allow-root
fi

# Si une commande est passée, on l'exécute. Sinon, on ouvre un shell.
if [ "$#" -gt 0 ]; then
  exec wp "$@" --allow-root
else
  exec /bin/sh
fi
EOF

chmod +x docker/scripts/wp-entrypoint.sh

Démarrage + installation automatique :

# Pensez à mapper wp.local vers 127.0.0.1 (ou utilisez localhost)
# Exemple Linux/macOS :
# echo "127.0.0.1 wp.local" | sudo tee -a /etc/hosts

docker compose up -d --build
docker compose run --rm wpcli

Commandes staging que j’utilise tout le temps :

# Export DB via WP-CLI (gère mieux certains contextes WP)
docker compose run --rm wpcli db export /var/www/html/wp-content/db.sql

# Import DB
docker compose run --rm wpcli db import /var/www/html/wp-content/db.sql

# Search-replace safe (sérialisation) : staging -> local
docker compose run --rm wpcli search-replace 'https://staging.exemple.com' 'http://wp.local' --all-tables --precise

# Reset complet (DANGER : efface tout)
docker compose run --rm wpcli db reset --yes
docker compose run --rm wpcli core install --url="http://wp.local" --title="Reset" --admin_user=admin --admin_password=admin [email protected] --skip-email

Note sur les page builders (Divi 5, Elementor, Avada) : ils sont sensibles aux URL et au cache. Le combo “import DB + mauvais search-replace + cache navigateur” produit des pages blanches ou des CSS manquants. Faites le search-replace via WP-CLI, puis purge cache builder :

  • Elementor : régénération CSS/Files (équivalent via CLI : souvent stocké en options, pas standard WP) ;
  • Divi 5 : purge du cache statique Divi (souvent via action admin, pas de commande core) ;
  • Avada : idem.

En pratique, je fais toujours : wp cache flush + suppression des dossiers de cache dans wp-content si votre stack en crée.

Étape 6 : Cache serveur et mails en dev (Redis, Mailpit)

Tester Redis en dev est utile quand vous chassez :

  • des incohérences de cache objet (options transients, fragments) ;
  • des comportements différents entre staging et local ;
  • des “ça ne se met pas à jour” sur Elementor/Divi après modification.

Démarrez avec le profil Redis :

docker compose --profile redis up -d

Côté WordPress, vous pouvez activer un plugin d’object-cache Redis. Je ne fournis pas ici un plugin “maison” : utilisez un plugin reconnu et maintenu, et vérifiez sa compatibilité WP 6.9.4. Techniquement, ce qui compte côté serveur : Redis répond et votre PHP a accès au host redis.

Mailpit : pratique pour intercepter les emails (reset password, WooCommerce). Démarrage :

docker compose --profile mail up -d
# UI sur http://localhost:8025

Pour router les mails vers Mailpit, le plus simple en dev est d’utiliser un plugin SMTP configuré sur mailpit:1025. Côté infra, vous évitez de laisser PHP envoyer via un MTA absent.

Fichiers de configuration complets

.htaccess (si vous passez en Apache)

Vous êtes sur Nginx ici, mais je fournis un .htaccess complet parce que beaucoup d’équipes alternent (ou déploient sur Apache). Copiez-le à la racine WordPress si vous basculez vers httpd.

cat > wp/.htaccess <<'EOF'
# WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
EOF

wp-config.php (version “dev Docker”)

Si vous préférez versionner un wp-config.php (plutôt que de le générer), voici une base compatible WordPress 6.9.4, avec variables d’environnement. Attention : ne commitez pas des salts réels si votre dépôt est public.

<?php
/**
 * wp-config.php pour environnement Docker (WordPress 6.9.4+)
 * Commentaires en français, adapté à PHP 8.1+
 */

declare(strict_types=1);

// DB via variables d'environnement (Compose)
define('DB_NAME', getenv('WORDPRESS_DB_NAME') ?: 'wordpress');
define('DB_USER', getenv('WORDPRESS_DB_USER') ?: 'wp');
define('DB_PASSWORD', getenv('WORDPRESS_DB_PASSWORD') ?: 'wp');
define('DB_HOST', getenv('WORDPRESS_DB_HOST') ?: 'db');

$table_prefix = getenv('WORDPRESS_TABLE_PREFIX') ?: 'wp_';

// Charset/collation recommandés
define('DB_CHARSET', 'utf8mb4');
define('DB_COLLATE', '');

// Debug
define('WP_DEBUG', (bool) (getenv('WORDPRESS_DEBUG') ?: true));
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', true);

// URLs (utile quand le host/port change)
if ($home = getenv('WP_HOME')) {
	define('WP_HOME', $home);
}
if ($siteurl = getenv('WP_SITEURL')) {
	define('WP_SITEURL', $siteurl);
}

// Clés/salts : en dev, vous pouvez les laisser, mais évitez en prod.
define('AUTH_KEY',         'dev-only-change-me');
define('SECURE_AUTH_KEY',  'dev-only-change-me');
define('LOGGED_IN_KEY',    'dev-only-change-me');
define('NONCE_KEY',        'dev-only-change-me');
define('AUTH_SALT',        'dev-only-change-me');
define('SECURE_AUTH_SALT', 'dev-only-change-me');
define('LOGGED_IN_SALT',   'dev-only-change-me');
define('NONCE_SALT',       'dev-only-change-me');

// Limiter les révisions en dev (facilite les dumps)
define('WP_POST_REVISIONS', 20);

// Désactiver l'éditeur de fichiers dans l'admin (bonne habitude)
define('DISALLOW_FILE_EDIT', true);

// Chemin absolu
if (!defined('ABSPATH')) {
	define('ABSPATH', __DIR__ . '/');
}

require_once ABSPATH . 'wp-settings.php';

nginx.conf (variante avec cache FastCGI optionnel)

Si vous voulez tester un cache “type prod” (FastCGI cache), voici une variante. En dev, je ne l’active que pour reproduire un bug. Sinon, vous oubliez de purger et vous vous tirez une balle dans le pied.

cat > docker/nginx/default.conf <<'EOF'
proxy_cache_path /tmp/nginx-cache levels=1:2 keys_zone=FASTCGI:10m inactive=60m max_size=512m;

server {
  listen 80;
  server_name _;

  root /var/www/html;
  index index.php;

  set $skip_cache 0;

  # Ne cachez pas l'admin, ni les utilisateurs loggés
  if ($request_method = POST) { set $skip_cache 1; }
  if ($query_string != "") { set $skip_cache 1; }
  if ($request_uri ~* "/wp-admin/|/wp-login.php") { set $skip_cache 1; }
  if ($http_cookie ~* "wordpress_logged_in_|comment_author") { set $skip_cache 1; }

  location / {
    try_files $uri $uri/ /index.php?$args;
  }

  location ~ .php$ {
    try_files $uri =404;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass php:9000;

    # Cache FastCGI (optionnel, dev avancé)
    fastcgi_cache FASTCGI;
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache $skip_cache;
    fastcgi_cache_valid 200 10m;

    add_header X-FastCGI-Cache $upstream_cache_status;
  }

  location ~* .(css|js|jpg|jpeg|png|gif|ico|svg|webp)$ {
    expires 7d;
    add_header Cache-Control "public, max-age=604800";
    try_files $uri =404;
  }
}
EOF

Vérification

Vous voulez des checks “sans UI”, reproductibles en CI ou en debug terminal.

1) Services Docker

docker compose ps
docker compose logs --tail=50 nginx
docker compose logs --tail=50 php
docker compose logs --tail=50 db

2) HTTP

curl -I "http://localhost:${APP_PORT_HTTP}/" | sed -n '1,15p'
curl -s "http://localhost:${APP_PORT_HTTP}/" | grep -i "<title>" || true

3) WP-CLI

docker compose run --rm wpcli core version
docker compose run --rm wpcli plugin list
docker compose run --rm wpcli option get siteurl
docker compose run --rm wpcli db check

4) DB (charset/collation)

docker compose exec -T db mariadb -u"${DB_USER}" -p"${DB_PASSWORD}" -e "SHOW VARIABLES LIKE 'character_set_%';" "${DB_NAME}" | head -n 30

Si ça ne marche pas

Diagnostic par étapes, sans supposer que “Docker est cassé”.

Tableau de diagnostic

Symptôme Cause probable Vérification Solution
Permaliens en 404 Réécriture Nginx incorrecte ou structure non configurée docker compose run --rm wpcli rewrite list wp rewrite structure '/%postname%/' --hard puis flush
502 Bad Gateway PHP-FPM down / fastcgi_pass incorrect docker compose logs php, docker compose exec php php-fpm -t Corriger fastcgi_pass php:9000, rebuild image
Erreur “Error establishing a database connection” DB pas prête, mauvais host, mauvais mdp docker compose exec db mariadb-admin ping Vérifier DB_HOST=db, healthcheck, variables env
Accents/emoji cassés Dump importé en latin1 / tables en collation mixte Check information_schema collations Re-dump en utf8mb4, corriger tables, re-import
Lenteur extrême Xdebug activé php -m | grep xdebug Mettre XDEBUG_ENABLE=0, restart
CSS/JS manquants (builder) URL incohérente après migration + cache wp option get home, inspect network wp search-replace + purge caches builder/navigateur

Logs à consulter

# Nginx
docker compose exec nginx sh -lc "tail -n 200 /var/log/nginx/error.log"

# PHP (erreurs runtime)
docker compose exec php sh -lc "php -i | grep -E 'error_log|display_errors' -n || true"
docker compose logs --tail=200 php

# MariaDB
docker compose logs --tail=200 db

Tests “bas niveau”

# PHP répond ?
docker compose exec php php -r 'echo "PHP OKn";'

# Connexion DB depuis le conteneur PHP
docker compose exec php php -r '
$mysqli = new mysqli(getenv("WORDPRESS_DB_HOST"), getenv("WORDPRESS_DB_USER"), getenv("WORDPRESS_DB_PASSWORD"), getenv("WORDPRESS_DB_NAME"));
if ($mysqli->connect_error) { fwrite(STDERR, $mysqli->connect_error . PHP_EOL); exit(1); }
echo "DB OKn";
'

Pièges et erreurs courantes

Ce sont des erreurs réalistes que je vois en audit/formation, et qui font perdre du temps.

  • Copier le code au mauvais endroit : ex. coller default.conf dans un autre chemin que celui monté par volume. Vérifiez le volumes: du service Nginx.
  • Oublier de rebuild : vous modifiez le Dockerfile, mais vous faites juste docker compose up -d. Faites docker compose up -d --build.
  • Oublier un point-virgule dans wp-config.php : résultat, écran blanc. Vérifiez docker compose logs php.
  • Confusion actions/filtres lors de tests plugins : vous pensez “Docker a cassé WordPress”, alors que votre snippet s’exécute trop tôt. Reproduisez avec WP_DEBUG_LOG et l’ordre des hooks.
  • Tester sur production sans sauvegarde : le réflexe “je fais un search-replace en prod” est un accident annoncé. Faites-le en staging Docker, puis exportez.
  • Permaliens non régénérés après import DB : exécutez wp rewrite flush --hard (et vérifiez Nginx try_files).
  • CSS/JS non chargés à cause d’un mauvais enqueue : vous suspectez Nginx. Vérifiez d’abord le HTML généré et les 404 d’assets dans access.log.
  • Snippet cassé par un thème enfant / plugin de snippets : en Docker, vous avez une version propre. Activez/désactivez par WP-CLI : wp plugin deactivate ....
  • Erreur liée à une version PHP trop ancienne : votre collègue est en PHP 8.3, vous en 8.1. Ici, vous fixez PHP_VERSION dans .env et tout le monde est aligné.
  • Code d’ancien tutoriel incompatible : beaucoup d’articles datent de PHP 7.x. Sur PHP 8.1+, les warnings/TypeError changent. Gardez error_reporting=E_ALL pour les voir.

Sécurité serveur

Même en dev, je garde des garde-fous. Les mauvaises habitudes migrent en prod.

  • Ne tournez pas en root pour écrire les fichiers WordPress. Sinon, vous finissez avec des permissions impossibles à gérer côté host.
  • Bloquez l’accès à wp-config.php, readme.html, license.txt côté Nginx (déjà fait plus haut).
  • Ne commitez pas de vrais salts/mots de passe. Utilisez un .env local non versionné si nécessaire.
  • Xdebug : ne l’exposez pas sur un réseau non maîtrisé. Ici, il est local et optionnel.
  • Headers : même en dev, gardez une baseline (nosniff, frame-options). Ça évite des surprises quand vous comparez staging/prod.
  • Volumes : évitez de monter tout votre $HOME dans un conteneur. Montez uniquement le projet.

Si vous voulez aller plus loin, regardez les recommandations serveur officielles WordPress : Requirements (wordpress.org).

Ressources

FAQ

Pourquoi Nginx + PHP-FPM plutôt qu’un conteneur “WordPress tout-en-un” ?

Parce que vous reproduisez une architecture proche prod, et vous isolez les responsabilités. Quand vous avez un 502, vous savez si c’est Nginx ou PHP-FPM.

Puis-je remplacer MariaDB par MySQL 8.x/8.4 ?

Oui. Remplacez l’image mariadb:10.11 par mysql:8.4 (ou la version cible prod) et adaptez les variables d’environnement (MYSQL_*). Gardez utf8mb4.

Comment changer la version de PHP sans casser le projet ?

Modifiez PHP_VERSION dans .env, puis :

docker compose down
docker compose up -d --build

Pourquoi mon WordPress affiche la mauvaise URL après import ?

Parce que home et siteurl viennent de la DB. Vérifiez :

docker compose run --rm wpcli option get home
docker compose run --rm wpcli option get siteurl

Puis faites un search-replace WP-CLI (pas un find/replace SQL brut) pour éviter de casser la sérialisation.

Comment activer Xdebug ponctuellement ?

Dans .env :

XDEBUG_ENABLE=1

Puis :

docker compose up -d --build php

Je vois des fichiers appartenant à root dans wp/. Pourquoi ?

Vous avez exécuté WP-CLI ou un script en root dans le conteneur. Corrigez côté host avec un chown (selon votre OS), puis évitez de lancer des commandes en root. C’est une source classique de “Git ne peut plus écrire”.

Comment ajouter Composer pour des projets plus “modernes” (Bedrock, mu-plugins) ?

Ajoutez composer à l’image PHP (téléchargement du phar) ou utilisez une image multi-stage. Gardez WP-CLI séparé si vous voulez limiter les surfaces. En équipe, je préfère une seule image “toolbox” (php + wp + composer) pour éviter les divergences.

Comment gérer un staging “clone prod” proprement ?

Workflow fiable :

  • export DB prod (ou staging) ;
  • import dans Docker ;
  • wp search-replace vers l’URL locale ;
  • wp rewrite flush --hard ;
  • purge caches (Redis/builder) si activés.

Redis me crée des comportements bizarres. C’est normal ?

Oui, si votre code n’invalide pas correctement le cache objet. Désactivez Redis pour confirmer :

docker compose stop redis
docker compose run --rm wpcli cache flush

Comment tester la restauration complète (disaster recovery) ?

Faites un cycle complet :

docker compose down -v
docker compose up -d --build
cat ./docker/mysql/backup.sql | docker compose exec -T db mariadb -uroot -p"${DB_ROOT_PASSWORD}" "${DB_NAME}"
docker compose run --rm wpcli rewrite flush --hard

Si ça passe, vos procédures sont réellement reproductibles.