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/htmlpour é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) oumysqldump.
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 :
- WP-CLI Commands (developer.wordpress.org)
- wp-config.php (wordpress.org)
- Nginx configuration for WordPress (developer.wordpress.org)
- OPcache configuration (php.net)
- WordPress core (GitHub mirror)
É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-replacefait “à 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.confdans un autre chemin que celui monté par volume. Vérifiez levolumes:du service Nginx. - Oublier de rebuild : vous modifiez le Dockerfile, mais vous faites juste
docker compose up -d. Faitesdocker compose up -d --build. - Oublier un point-virgule dans
wp-config.php: résultat, écran blanc. Vérifiezdocker 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_LOGet 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 Nginxtry_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_VERSIONdans.envet 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_ALLpour 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.txtcôté Nginx (déjà fait plus haut). - Ne commitez pas de vrais salts/mots de passe. Utilisez un
.envlocal 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
$HOMEdans un conteneur. Montez uniquement le projet.
Si vous voulez aller plus loin, regardez les recommandations serveur officielles WordPress : Requirements (wordpress.org).
Ressources
- Nginx et WordPress (developer.wordpress.org)
- WP-CLI commandes (developer.wordpress.org)
- Prérequis serveur WordPress (wordpress.org)
- Éditer wp-config.php (wordpress.org)
- Configuration OPcache (php.net)
- wordpress-develop (GitHub)
- WordPress Core Trac (tickets, historique)
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-replacevers 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.