Si vous avez déjà perdu une demi-journée parce que “ça marche sur ma machine” mais pas sur celle d’un collègue, Docker est souvent le point de bascule. Le vrai gain n’est pas “d’avoir WordPress dans un conteneur”, c’est de figer toutes les dépendances (PHP, extensions, base de données, Redis, mail, outils) et de pouvoir reconstruire l’environnement à l’identique.

Ce qu’on va construire

Vous allez mettre en place un environnement de développement WordPress reproductible, pensé pour WordPress 6.9.4 (avril 2026) et PHP 8.3+ (PHP minimum recommandé : 8.1). L’objectif est d’obtenir une stack “proche prod” mais orientée dev : Xdebug, WP-CLI, logs lisibles, capture des e-mails, cache Redis optionnel, et un bootstrap automatisé.

Ce setup est adapté à :

  • Sites vitrine et blogs avec thème sur mesure (ou thème enfant),
  • Sites builder (Divi 5, Elementor, Avada) où vous voulez isoler les plugins et les versions,
  • Agences qui doivent onboarder vite un nouveau dev ou reproduire un bug client.

À la fin, vous saurez :

  • lancer WordPress 6.9.4 en une commande,
  • installer automatiquement WP, plugins et données de base,
  • debugger proprement (Xdebug + logs),
  • capturer les e-mails sortants sans toucher à un SMTP réel,
  • versionner la config sans versionner les secrets.

Résumé rapide

  • On part d’un projet Git avec .env (non versionné) + .env.example (versionné).
  • On utilise docker compose avec services : wp, db, redis, mailpit, traefik (option).
  • On construit une image wp-dev basée sur wordpress:php8.3-apache + Xdebug + WP-CLI.
  • On automatise l’installation via WP-CLI (idempotent) et on garde les uploads en volume.
  • On force une stratégie logs + debug stable : WP_DEBUG_LOG, logs Apache, Xdebug en “trigger”.

Quand utiliser cette solution

Je la recommande quand vous avez au moins un de ces besoins :

  • Reproduire un bug client : vous figez versions PHP/extensions, MySQL/MariaDB, et vous évitez les “diffs invisibles”.
  • Travailler en équipe : même stack pour tout le monde, y compris CI.
  • Maintenir plusieurs projets : chaque projet embarque sa stack, sans polluer votre machine.
  • Tester des plugins lourds (builders, WooCommerce) sans casser votre environnement local.

Dans mon expérience, c’est particulièrement utile sur des sites Elementor/Avada où une extension PHP manquante (intl, zip, gd) fait “juste” planter un import de template, sans message exploitable.

Quand ne PAS utiliser cette solution

  • Si vous êtes sur une machine très limitée (RAM/CPU) et que votre projet est minuscule : Docker peut être plus lent qu’un PHP local.
  • Si vous faites uniquement du contenu (pas de dev) : un staging managé est souvent plus simple.
  • Si votre équipe n’a pas la discipline de versionner correctement .env.example et les scripts : vous allez créer un “Docker qui marche chez une seule personne”, ce qui est pire qu’avant.

Avant de commencer (prérequis)

Préparez-vous comme si vous alliez casser quelque chose, parce que vous allez casser quelque chose.

Prérequis techniques

  • Docker Desktop (macOS/Windows) ou Docker Engine (Linux) + plugin Compose. Docs : Docker Compose.
  • Git.
  • Un éditeur qui gère bien les fins de ligne (VS Code par exemple).

Versions ciblées

  • WordPress : 6.9.4 (vous pouvez changer la version plus tard, mais on part sur celle-ci).
  • PHP : 8.3 dans l’image (compatible WP 6.9.4, et au-dessus du minimum 8.1).
  • MariaDB : 10.11 LTS (bon compromis stabilité/compat).

Sauvegarde & environnement

  • Ne testez pas ça sur un site en production. Faites-le dans un dépôt dédié.
  • Si vous migrez un site existant : export DB + wp-content/uploads avant de commencer.

Sécurité (local mais pas “sans risque”)

  • Ne commitez jamais .env (mots de passe DB, salts, clés).
  • Évitez d’exposer MySQL/Redis sur 0.0.0.0 si ce n’est pas nécessaire.

Références officielles utiles :


Étape 1 : Créer une structure de projet propre et versionnable

Objectif : séparer ce qui est versionné (config, scripts) de ce qui ne l’est pas (secrets, volumes, uploads).

1) Créez l’arborescence

Dans un dossier vide :

mkdir -p wp-docker/{docker,bin,wp,wp-content,logs}
cd wp-docker

Vous allez obtenir :

  • docker/ : Dockerfile, conf Apache/PHP, scripts d’entrée
  • bin/ : scripts utilitaires (install, reset, import db…)
  • wp/ : WordPress core (monté en volume) ou docroot selon variante
  • wp-content/ : thèmes/plugins mu-plugins (optionnel) versionnés
  • logs/ : logs persistants

2) Ajoutez un .gitignore strict

Créez .gitignore à la racine :

cat > .gitignore <<'EOF'
# Secrets
.env

# Volumes / données locales
data/
logs/*.log
wp/wp-config.php

# Dépendances éventuelles
node_modules/
vendor/

# OS / IDE
.DS_Store
.idea/
.vscode/
EOF

3) Créez vos fichiers d’environnement

Créez .env.example (versionné) :

cat > .env.example <<'EOF'
# Domaine local (utilisé par WP_HOME/WP_SITEURL)
WP_HOST=wp.local

# WordPress
WP_VERSION=6.9.4
WP_ENV=development

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

# Ports
HTTP_PORT=8080
MAILPIT_PORT=8025

# Xdebug
XDEBUG_MODE=debug,develop
XDEBUG_TRIGGER=1
EOF

Puis copiez-le en .env (non versionné) :

cp .env.example .env

Résultat attendu

Vous avez un dépôt prêt à accueillir docker-compose.yml sans secrets committés. C’est le point où beaucoup se trompent : j’ai souvent vu des mots de passe DB dans Git “parce que c’est du local”. Le jour où un repo devient public par erreur, c’est trop tard.


Étape 2 : Écrire un docker-compose reproductible (PHP 8.3+, MariaDB, Redis)

Objectif : déclarer une stack stable, avec des volumes nommés, et des ports explicites.

1) Créez docker-compose.yml

À la racine, créez docker-compose.yml :

cat > docker-compose.yml <<'EOF'
services:
  wp:
    build:
      context: .
      dockerfile: docker/Dockerfile
    container_name: wp_app
    env_file: .env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
      mailpit:
        condition: service_started
    ports:
      - "${HTTP_PORT:-8080}:80"
    volumes:
      # Code WordPress (core) + contenu
      - ./wp:/var/www/html
      - ./wp-content:/var/www/html/wp-content
      # Logs persistants
      - ./logs:/var/log/wp
    extra_hosts:
      # Permet à Xdebug de joindre l'hôte sur Linux (optionnel sur Mac/Windows)
      - "host.docker.internal:host-gateway"

  db:
    image: mariadb:10.11
    container_name: wp_db
    environment:
      MYSQL_DATABASE: ${DB_NAME:-wordpress}
      MYSQL_USER: ${DB_USER:-wordpress}
      MYSQL_PASSWORD: ${DB_PASSWORD:-wordpress}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root}
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mariadb-admin", "ping", "-h", "127.0.0.1", "-uroot", "-p${DB_ROOT_PASSWORD}"]
      interval: 5s
      timeout: 3s
      retries: 30

  redis:
    image: redis:7-alpine
    container_name: wp_redis
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis_data:/data

  mailpit:
    image: axllent/mailpit:latest
    container_name: wp_mailpit
    ports:
      - "${MAILPIT_PORT:-8025}:8025"

volumes:
  db_data:
  redis_data:
EOF

2) Pourquoi ces choix

  • Volumes nommés (db_data, redis_data) : vous évitez de versionner des données et vous pouvez reset facilement.
  • Healthcheck DB : sans ça, WP-CLI peut tenter l’install avant que MariaDB ne soit prête (race condition classique).
  • Mailpit : vous voyez les mails sortants, sans SMTP. Très pratique pour les formulaires.

Résultat attendu

À ce stade, docker compose up échouera encore car l’image wp n’existe pas. C’est normal : on la construit à l’étape suivante.


Étape 3 : Construire une image WordPress “dev” (Xdebug, WP-CLI, extensions)

Objectif : ne pas dépendre d’un conteneur “wordpress vanilla” dès que vous avez besoin d’outils (WP-CLI, Xdebug, zip, intl…). Je préfère une image dédiée, car sinon vous finissez par bricoler à la main dans le conteneur, et ce n’est plus reproductible.

1) Créez docker/Dockerfile

cat > docker/Dockerfile <<'EOF'
FROM wordpress:php8.3-apache

# Paquets système utiles en dev (zip, intl, mysql client, etc.)
RUN apt-get update && apt-get install -y --no-install-recommends 
    git 
    unzip 
    less 
    mariadb-client 
    libzip-dev 
    libicu-dev 
  && docker-php-ext-install zip intl 
  && rm -rf /var/lib/apt/lists/*

# Activer mod_rewrite (permalinks)
RUN a2enmod rewrite headers

# Xdebug (install via PECL)
RUN pecl install xdebug 
  && docker-php-ext-enable xdebug

# WP-CLI (binaire 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

# Configuration PHP (logs, limites)
COPY docker/php.ini /usr/local/etc/php/conf.d/99-wp-dev.ini

# Configuration Apache (DocumentRoot, logs)
COPY docker/apache.conf /etc/apache2/sites-available/000-default.conf

# Script d'entrée (bootstrap idempotent)
COPY docker/entrypoint.sh /usr/local/bin/wp-entrypoint
RUN chmod +x /usr/local/bin/wp-entrypoint

ENTRYPOINT ["wp-entrypoint"]
CMD ["apache2-foreground"]
EOF

2) Ajoutez docker/php.ini

cat > docker/php.ini <<'EOF'
; Logs PHP vers stderr + fichier (pratique en debug)
log_errors = On
error_reporting = E_ALL
display_errors = Off

; Ajustez selon vos besoins
memory_limit = 512M
upload_max_filesize = 128M
post_max_size = 128M
max_execution_time = 120

; Xdebug en mode "trigger" pour éviter de ralentir tout le monde
xdebug.mode = ${XDEBUG_MODE}
xdebug.start_with_request = trigger
xdebug.trigger_value = ${XDEBUG_TRIGGER}

; Sur Mac/Windows: host.docker.internal marche.
; Sur Linux: extra_hosts dans compose.
xdebug.client_host = host.docker.internal
xdebug.client_port = 9003
xdebug.log_level = 0
EOF

3) Ajoutez docker/apache.conf

cat > docker/apache.conf <<'EOF'
<VirtualHost *:80>
  ServerAdmin webmaster@localhost
  DocumentRoot /var/www/html

  <Directory /var/www/html>
    AllowOverride All
    Require all granted
  </Directory>

  ErrorLog /var/log/wp/apache-error.log
  CustomLog /var/log/wp/apache-access.log combined
</VirtualHost>
EOF

4) Ajoutez docker/entrypoint.sh

Ce script fait trois choses : attendre la DB, télécharger WP si absent, générer un wp-config.php propre (avec salts et debug).

cat > docker/entrypoint.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

# Petit helper de logs
log() {
  echo "[wp-entrypoint] $*"
}

# Attendre MariaDB (évite la course au démarrage)
wait_for_db() {
  log "Attente de la base de données ${DB_HOST:-db}..."
  for i in {1..60}; do
    if mariadb-admin ping -h"${DB_HOST:-db}" -u"${DB_USER:-wordpress}" -p"${DB_PASSWORD:-wordpress}" --silent; then
      log "Base de données prête."
      return 0
    fi
    sleep 2
  done
  log "ERREUR: la base n'est pas prête après 120s."
  return 1
}

# Télécharger WordPress si /var/www/html est vide (ou sans wp-includes)
ensure_wordpress_core() {
  if [ ! -d /var/www/html/wp-includes ]; then
    log "WordPress core absent, téléchargement de WP ${WP_VERSION:-6.9.4}..."
    wp core download --version="${WP_VERSION:-6.9.4}" --path=/var/www/html --allow-root
  else
    log "WordPress core déjà présent."
  fi
}

# Générer wp-config.php si absent
ensure_wp_config() {
  if [ ! -f /var/www/html/wp-config.php ]; then
    log "Génération de wp-config.php..."
    wp config create 
      --path=/var/www/html 
      --dbname="${DB_NAME:-wordpress}" 
      --dbuser="${DB_USER:-wordpress}" 
      --dbpass="${DB_PASSWORD:-wordpress}" 
      --dbhost="${DB_HOST:-db}" 
      --skip-check 
      --allow-root

    # Ajouter des constantes utiles en dev (et Redis en option)
    wp config set WP_ENVIRONMENT_TYPE "${WP_ENV:-development}" --type=constant --allow-root
    wp config set WP_DEBUG true --type=constant --allow-root
    wp config set WP_DEBUG_LOG true --type=constant --allow-root
    wp config set WP_DEBUG_DISPLAY false --type=constant --allow-root
    wp config set SCRIPT_DEBUG true --type=constant --allow-root

    # Logs WordPress vers un fichier monté
    wp config set WP_CONTENT_DIR "/var/www/html/wp-content" --type=constant --allow-root
    wp config set WP_CONTENT_URL "http://localhost/wp-content" --type=constant --allow-root

    # Redis (si plugin présent plus tard)
    wp config set WP_REDIS_HOST "redis" --type=constant --allow-root
    wp config set WP_REDIS_PORT 6379 --type=constant --allow-root
  else
    log "wp-config.php déjà présent."
  fi
}

wait_for_db
ensure_wordpress_core
ensure_wp_config

exec "$@"
EOF

Résultat attendu

L’image est prête. Vous avez un point de bootstrap unique, versionnable, et vous évitez le piège classique “j’ai installé WP-CLI dans mon conteneur à la main, mais personne d’autre ne l’a”.


Étape 4 : Installer WordPress 6.9.4 et figer la configuration

Objectif : démarrer les conteneurs, puis faire l’installation WordPress de manière contrôlée (et reproductible).

1) Démarrez la stack

docker compose up -d --build

Vérifiez :

2) Installez WordPress via WP-CLI

Exécutez :

docker compose exec wp wp core install 
  --url="http://localhost:8080" 
  --title="WP Docker Dev" 
  --admin_user="admin" 
  --admin_password="admin" 
  --admin_email="[email protected]" 
  --skip-email 
  --allow-root

Oui, le mot de passe est volontairement faible : on est en local. Ne copiez pas ce pattern en staging public.

3) Réglez les permaliens

Sans ça, vous allez croire que mod_rewrite est cassé :

docker compose exec wp wp rewrite structure '/%postname%/' --hard --allow-root

Résultat attendu

Vous pouvez vous connecter à /wp-admin, créer un article, et les URLs “jolies” fonctionnent.


Étape 5 : Automatiser l’installation et les données (WP-CLI, scripts)

Objectif : une commande = environnement prêt. C’est ici que la reproductibilité devient réelle.

1) Créez un script bin/install.sh

cat > bin/install.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

# Script d'installation idempotent pour WordPress en Docker
# Usage: ./bin/install.sh

HTTP_PORT="${HTTP_PORT:-8080}"

docker compose up -d --build

# Attendre que WP réponde (évite les installs trop rapides)
echo "[install] Attente HTTP..."
for i in {1..60}; do
  if curl -sSf "http://localhost:${HTTP_PORT}/wp-login.php" > /dev/null; then
    break
  fi
  sleep 2
done

# Installer WP si pas déjà installé
if docker compose exec -T wp wp core is-installed --allow-root > /dev/null 2>&1; then
  echo "[install] WordPress déjà installé."
else
  echo "[install] Installation WordPress..."
  docker compose exec -T wp wp core install 
    --url="http://localhost:${HTTP_PORT}" 
    --title="WP Docker Dev" 
    --admin_user="admin" 
    --admin_password="admin" 
    --admin_email="[email protected]" 
    --skip-email 
    --allow-root

  docker compose exec -T wp wp rewrite structure '/%postname%/' --hard --allow-root
fi

# Installer des plugins utiles en dev
echo "[install] Installation plugins dev..."
docker compose exec -T wp wp plugin install query-monitor --activate --allow-root

# Redis cache (optionnel) : on l'installe mais on ne l'active pas si vous ne voulez pas
docker compose exec -T wp wp plugin install redis-cache --allow-root || true

# Réglages de base
docker compose exec -T wp wp option update blogdescription "Environnement reproductible Docker" --allow-root

echo "[install] OK. WP: http://localhost:${HTTP_PORT}  Admin: admin/admin"
EOF

chmod +x bin/install.sh

2) Lancez le bootstrap

./bin/install.sh

3) Pourquoi c’est mieux qu’un README

Un README ment toujours à un moment. Un script, lui, casse tout de suite si une étape manque. Et quand ça casse, vous savez où.

Résultat attendu

Vous pouvez supprimer vos volumes, relancer ./bin/install.sh, et retrouver un WP fonctionnel, avec Query Monitor activé.


Étape 6 : Debug & logs (WP_DEBUG, Xdebug, Query Monitor, error_log)

Objectif : rendre les bugs visibles, sans ralentir toute la stack.

1) Où lire les logs

  • Logs Apache : logs/apache-error.log et logs/apache-access.log
  • Log WordPress : par défaut wp-content/debug.log (car WP_DEBUG_LOG est à true)

Commande pratique :

tail -f logs/apache-error.log wp-content/debug.log

2) Xdebug sans tout ralentir

Le setup utilise xdebug.start_with_request=trigger. Concrètement :

  • Sans trigger : Xdebug ne s’active pas, perf correcte.
  • Avec trigger : vous activez le debug uniquement quand vous en avez besoin.

Exemple : ajoutez un paramètre ?XDEBUG_TRIGGER=1 à une URL, ou configurez votre IDE pour envoyer le cookie/trigger. Référence : Xdebug Step Debug.

3) Erreur réaliste : “Fatal error … call to undefined function”

Je vois souvent ça quand un snippet est collé dans le mauvais fichier (ou exécuté trop tôt). Dans WordPress, certains hooks n’existent pas “tout de suite”. Si vous testez un plugin mu-plugin, assurez-vous d’accrocher votre code à un hook approprié.

Mini mu-plugin de test (à versionner) : créez wp-content/mu-plugins/dev-tools.php :

<?php
/**
 * Plugin Name: Dev Tools (local)
 * Description: Aides au debug en environnement Docker.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

add_action( 'init', function () {
	// Exemple : log contrôlé (évitez var_dump en prod)
	if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
		error_log( '[dev-tools] init OK, WP ' . get_bloginfo( 'version' ) );
	}
}, 20 );

Résultat : une ligne apparaît dans wp-content/debug.log à chaque requête.


Étape 7 : Capturer les e-mails et fiabiliser le cron (Mailpit + cron système)

Objectif : arrêter de “tester” des e-mails en envoyant de vrais messages, et éviter les surprises liées à WP-Cron.

1) Capturer les e-mails

Mailpit capture ce qui sort via mail(). Beaucoup de plugins utilisent PHPMailer via WordPress, qui finit souvent en sendmail dans un conteneur. Selon les images, ça peut varier.

Approche fiable : installer un plugin SMTP local pointant vers Mailpit (ou configurer PHPMailer via hook). Pour rester reproductible et sans UI, je préfère un mu-plugin.

Créez wp-content/mu-plugins/mailpit.php :

<?php
/**
 * Plugin Name: Mailpit SMTP (local)
 * Description: Force l'envoi SMTP vers Mailpit en dev.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

add_action( 'phpmailer_init', function ( $phpmailer ) {
	// En local uniquement : si vous mettez ça en prod, vous allez perdre vos e-mails.
	if ( defined( 'WP_ENVIRONMENT_TYPE' ) && WP_ENVIRONMENT_TYPE !== 'development' ) {
		return;
	}

	$phpmailer->isSMTP();
	$phpmailer->Host       = 'mailpit';
	$phpmailer->Port       = 1025;
	$phpmailer->SMTPAuth   = false;
	$phpmailer->SMTPSecure = '';
}, 10, 1 );

Test :

docker compose exec wp wp eval "wp_mail('[email protected]','Test','Hello');" --allow-root

Ouvrez Mailpit : http://localhost:8025.

2) Remplacer WP-Cron par un cron système (en dev)

WP-Cron dépend du trafic. En dev, c’est aléatoire. Je remplace souvent WP-Cron par un cron toutes les minutes.

Ajoutez dans wp-config.php (déjà généré) :

docker compose exec wp wp config set DISABLE_WP_CRON true --type=constant --allow-root

Puis ajoutez un service cron dans Compose (option avancée). Éditez docker-compose.yml et ajoutez :

cat >> docker-compose.yml <<'EOF'

  cron:
    image: curlimages/curl:8.7.1
    container_name: wp_cron
    depends_on:
      - wp
    command: >
      sh -lc "while true; do
        curl -sS http://wp/wp-cron.php?doing_wp_cron=1 > /dev/null || true;
        sleep 60;
      done"
EOF

Redémarrez :

docker compose up -d

Résultat : vos tâches planifiées (imports, sync, newsletters en dev) se déclenchent de manière stable.


Étape 8 : Ajouter des tests rapides (PHPUnit + wp-env en option)

Objectif : avoir un filet de sécurité minimal. Même sur un blog, un plugin maison ou un thème sur mesure mérite au moins quelques tests.

Option A : PHPUnit dans le conteneur (simple, rapide)

Vous pouvez installer PHPUnit via Composer dans votre thème/plugin. Si votre projet a déjà Composer, c’est le bon moment. Sinon, vous pouvez limiter à des tests manuels.

Exemple (plugin custom) : structurez un plugin wp-content/plugins/my-plugin et ajoutez Composer. Je ne détaille pas toute la mise en place ici, car ça dépend fortement de votre architecture, mais retenez ceci : gardez les tests hors du runtime WordPress.

Option B : wp-env (outil officiel dev core) pour tests isolés

@wordpress/env (wp-env) est pratique pour lancer un WP jetable pour tests, sans impacter votre stack Docker principale. C’est utile en CI.

Doc officielle : @wordpress/env.


Le résultat complet

Si vous voulez tout copier d’un coup, voici les fichiers clés assemblés. Personnalisez ensuite via .env et les mu-plugins.

docker-compose.yml (complet)

services:
  wp:
    build:
      context: .
      dockerfile: docker/Dockerfile
    container_name: wp_app
    env_file: .env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
      mailpit:
        condition: service_started
    ports:
      - "${HTTP_PORT:-8080}:80"
    volumes:
      - ./wp:/var/www/html
      - ./wp-content:/var/www/html/wp-content
      - ./logs:/var/log/wp
    extra_hosts:
      - "host.docker.internal:host-gateway"

  db:
    image: mariadb:10.11
    container_name: wp_db
    environment:
      MYSQL_DATABASE: ${DB_NAME:-wordpress}
      MYSQL_USER: ${DB_USER:-wordpress}
      MYSQL_PASSWORD: ${DB_PASSWORD:-wordpress}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root}
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mariadb-admin", "ping", "-h", "127.0.0.1", "-uroot", "-p${DB_ROOT_PASSWORD}"]
      interval: 5s
      timeout: 3s
      retries: 30

  redis:
    image: redis:7-alpine
    container_name: wp_redis
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis_data:/data

  mailpit:
    image: axllent/mailpit:latest
    container_name: wp_mailpit
    ports:
      - "${MAILPIT_PORT:-8025}:8025"

  cron:
    image: curlimages/curl:8.7.1
    container_name: wp_cron
    depends_on:
      - wp
    command: >
      sh -lc "while true; do
        curl -sS http://wp/wp-cron.php?doing_wp_cron=1 > /dev/null || true;
        sleep 60;
      done"

volumes:
  db_data:
  redis_data:

Personnalisations utiles

  • Changer HTTP_PORT si vous avez déjà un serveur local sur 8080.
  • Remplacer MariaDB par MySQL 8 si votre prod est en MySQL (attention aux collations/SQL modes).
  • Activer Redis côté WP en installant/configurant correctement le plugin (voir section maintenance).

Adapter pour Divi 5 / Elementor / Avada

Les builders ne changent pas Docker, mais ils changent vos contraintes : mémoire, temps d’exécution, taille d’upload, et parfois des dépendances PHP (zip, intl, gd/imagemagick).

Divi 5

  • Augmentez memory_limit (déjà à 512M) si vous importez des layouts lourds.
  • Gardez wp-content versionné partiellement : thème enfant + mu-plugins, mais pas les caches.

Elementor

  • Elementor déclenche souvent des requêtes AJAX longues : max_execution_time à 120s évite des faux négatifs.
  • Si des CSS/JS ne se chargent pas : vérifiez les permaliens et les headers cache (souvent un mauvais AllowOverride ou un cache navigateur).

Avada (Fusion Builder)

  • Imports de démos : assurez-vous d’avoir zip et intl (installés dans le Dockerfile), sinon vous aurez des erreurs silencieuses.
  • Avada peut générer beaucoup de fichiers : gardez wp-content/uploads en volume (c’est déjà le cas via ./wp-content).

Vérification finale

  1. Accès front : http://localhost:8080 affiche le site.
  2. Accès admin : /wp-admin fonctionne, login OK.
  3. Permaliens : un article en /%postname%/ ne renvoie pas 404.
  4. Logs : une erreur PHP volontaire apparaît dans wp-content/debug.log (testez avec un error_log('test') dans un mu-plugin).
  5. Mail : un wp_mail() apparaît dans Mailpit.
  6. Xdebug : en ajoutant ?XDEBUG_TRIGGER=1, votre IDE accroche une requête (si configuré).

Si le résultat n’est pas celui attendu

Voici un tableau de diagnostic que j’utilise en premier passage. Il couvre les symptômes les plus fréquents sur des stacks Docker WordPress.

Symptôme Cause probable Vérification Solution
Page blanche / 500 Erreur PHP, plugin incompatible, mémoire Lire logs/apache-error.log et wp-content/debug.log Désactiver plugin via WP-CLI, augmenter memory_limit, corriger l’erreur
Erreur “Error establishing a database connection” DB pas prête, mauvais identifiants, volumes corrompus docker compose logs db + test mariadb-admin ping Vérifier .env, supprimer volume DB et relancer install
Permaliens 404 mod_rewrite off / AllowOverride bloqué Vérifier Apache conf et a2enmod rewrite Rebuild image, relancer wp rewrite structure ... --hard
Xdebug ne se connecte pas Mauvais host/port, trigger absent IDE écoute 9003 ? trigger envoyé ? Configurer IDE, utiliser host.docker.internal, vérifier firewall
Les e-mails n’apparaissent pas Plugin SMTP override, PHPMailer non configuré Tester wp_mail() + logs Activer le mu-plugin Mailpit, vérifier host mailpit port 1025

Commandes de dépannage (réflexes)

# Voir l'état
docker compose ps

# Logs d'un service
docker compose logs -f wp
docker compose logs -f db

# Entrer dans le conteneur WP
docker compose exec wp bash

# Désactiver tous les plugins (si wp-admin inaccessible)
docker compose exec wp wp plugin deactivate --all --allow-root

# Reconstruire proprement
docker compose down
docker compose up -d --build

Pièges et erreurs courantes

Erreur Cause Solution
Copier un snippet dans functions.php du mauvais thème Vous éditez le thème parent au lieu du thème enfant Versionnez un thème enfant, ou utilisez un mu-plugin pour le code dev
Oublier un point-virgule dans un mu-plugin Parse error immédiate, parfois page blanche Regarder debug.log et apache-error.log, corriger, recharger
Confusion action/filtre (hook inadapté) Le code ne s’exécute pas, ou trop tôt Utiliser init/wp_loaded selon le besoin, vérifier priorité
Tester un ancien tutoriel (WP 5.x) avec du code obsolète Fonctions/constantes changées, comportement différent Vérifier la doc officielle WP 6.9+ sur developer.wordpress.org
CSS/JS non chargés Mauvais wp_enqueue_script, mauvais hook, cache navigateur Enqueue sur wp_enqueue_scripts/admin_enqueue_scripts, vider cache
WP-CLI échoue “Could not connect to database” DB pas prête (race condition) Healthcheck + attente dans entrypoint (déjà inclus)
Erreur liée à PHP trop ancien Vous avez changé l’image PHP sans rebuild docker compose build --no-cache, vérifier php -v dans le conteneur

Variante / alternative

Alternative 1 : LocalWP / DevKinsta (sans Docker “à la main”)

Si votre objectif est surtout de développer un thème ou un site builder sans gérer l’infra, des outils comme LocalWP peuvent être plus rapides à prendre en main. Le revers : la reproductibilité en équipe est moins “déclarative” qu’un repo Docker, et l’intégration CI est souvent plus compliquée.

Alternative 2 (avancée) : Traefik + HTTPS + domaines multiples

Quand vous gérez 10 projets en parallèle, je passe souvent sur Traefik pour éviter la guerre des ports et avoir du HTTPS local. C’est plus long à configurer, mais ensuite vous avez :

  • https://projet-a.test, https://projet-b.test
  • certificats auto (mkcert ou Traefik + CA locale)
  • une stack multi-projets propre

Je ne l’active pas par défaut ici pour rester focalisé sur WordPress, mais c’est une extension naturelle.


Conseils sécurité, performance et maintenance

Sécurité

  • Ne publiez pas votre port 3306 (DB) vers l’hôte si ce n’est pas nécessaire.
  • Gardez WP_DEBUG_DISPLAY à false même en dev : vous évitez de “debugger” via HTML cassé.
  • Si vous exposez le site sur un réseau (démo client), changez les mots de passe et limitez l’accès.

Performance

  • Xdebug en trigger : c’est déjà un gros gain.
  • Évitez d’activer un cache agressif en dev (minify, concat) tant que vous débuggez.
  • Opcache : en dev pur, je le laisse souvent désactivé. Si vous voulez simuler la prod, activez-le et invalidez proprement.

Maintenance

  • Figez les versions d’images (MariaDB, Redis). Évitez latest sauf pour un service non critique (Mailpit peut rester en latest).
  • Mettez à jour WordPress via WP-CLI, pas en “cliquant” : vous gardez un historique et vous pouvez automatiser.

Commandes utiles :

# Mettre à jour WP (exemple)
docker compose exec wp wp core update --allow-root

# Vérifier version WP
docker compose exec wp wp core version --allow-root

# Vérifier PHP
docker compose exec wp php -v

# Nettoyer volumes (ATTENTION: supprime la DB)
docker compose down -v

Pour aller plus loin

  • Ajouter un service phpMyAdmin (ou Adminer) uniquement si votre équipe en a besoin. Je préfère WP-CLI + dumps SQL, mais certains workflows exigent une UI.
  • Ajouter un Makefile (ou justfile) pour standardiser les commandes (make up, make reset, make test).
  • Mettre en place un import DB “safe” : dump compressé + remplacement d’URLs via wp search-replace avec --skip-columns=guid.
  • Créer une image “prod-like” (sans Xdebug, avec opcache) et une image “dev”. Même Compose, deux profils.

Ressources


FAQ

Pourquoi monter ./wp en volume au lieu de builder le core dans l’image ?

Parce que vous voulez pouvoir changer rapidement de version WordPress (6.9.4 → 6.9.5) via WP-CLI et versionner vos scripts, pas reconstruire une image à chaque patch. Pour des projets très contrôlés, vous pouvez aussi “vendoriser” le core, mais c’est une autre philosophie.

Est-ce que ce setup marche avec PHP 8.1 exactement ?

Oui si vous changez l’image de base (wordpress:php8.1-apache). Je vise PHP 8.3 ici parce que ça reflète mieux les environnements actuels. Si votre prod est en 8.1, alignez-vous dessus pour éviter les surprises.

Comment importer une base client ?

Le plus fiable : dump SQL + wp db import puis wp search-replace. Exemple :

docker compose exec -T wp wp db import /var/www/html/wp-content/uploads/dump.sql --allow-root
docker compose exec wp wp search-replace 'https://www.site-client.tld' 'http://localhost:8080' --skip-columns=guid --allow-root

Pourquoi WP_CONTENT_URL est fixé à http://localhost/wp-content dans l’entrypoint ?

C’est un raccourci qui peut être faux si vous changez le port. Si vous utilisez un port différent, ajustez-le (ou supprimez ces deux constantes et laissez WordPress gérer). Je l’ai vu résoudre des cas tordus quand des reverse proxies locaux réécrivent les headers, mais ce n’est pas obligatoire.

Redis est lancé, mais WordPress ne l’utilise pas. Normal ?

Oui. Redis côté serveur ne suffit pas : il faut un plugin de cache objet (ex. redis-cache) et, selon le plugin, activer le cache via WP-CLI ou l’admin. En dev, je l’installe mais je ne l’active pas toujours.

Pourquoi Query Monitor en dev ?

Parce que c’est le moyen le plus rapide d’identifier une requête lente, un hook appelé trop souvent, ou un appel HTTP bloquant. Sur des sites builder, c’est souvent la différence entre “je suppose” et “je sais”.

Je vois “Permission denied” sur des fichiers dans wp-content. Que faire ?

Ça arrive quand l’UID/GID de l’hôte et du conteneur ne sont pas alignés (surtout Linux). La solution propre est d’exécuter Apache/PHP avec un user correspondant, ou de corriger les permissions sur votre dossier projet. Évitez le réflexe chmod -R 777.

Est-ce que je peux utiliser Nginx au lieu d’Apache ?

Oui, mais vous devrez gérer PHP-FPM, la config Nginx et les règles de permaliens. Apache est plus direct pour un tutoriel reproductible, surtout si vous voulez minimiser les différences entre environnements.

Comment vérifier la version exacte de WordPress dans le conteneur ?

docker compose exec wp wp core version --allow-root

Où suivre les changements core si un comportement change entre 6.9.x et 6.10 ?

Regardez les tickets et commits sur Trac et GitHub :