Docker pour les développeurs PHP : setup local et bonnes pratiques

Docker remplace WAMP, XAMPP et MAMP par un environnement de développement isolé, reproductible et documenté. Chaque projet a sa propre version de PHP, ses propres extensions, sa propre configuration Apache — sans conflit avec les autres projets. En 2026, la quasi-totalité des équipes de développement utilisent Docker pour leurs environnements locaux. Ce guide couvre la mise en place d'un setup Docker complet pour un projet PHP avec Apache, Node.js et MailHog.

Architecture d'un environnement Docker PHP

Architecture Docker PHP Schéma montrant deux conteneurs Docker : le conteneur web principal (PHP 8.5 + Apache + Node.js + Composer) qui sert le site sur les ports 8080/8443, et le conteneur MailHog qui intercepte les emails sur le port 8025. Les deux partagent un réseau Docker et le code source est monté via un volume. Docker Network Conteneur Web PHP 8.5 + Apache Node.js 24 (nvm) Composer Extensions PHP Ports : :8080 :8443 Volume : ./ → /var/www/html MailHog SMTP :1025 Web UI :8025 Intercepte les emails SMTP Navigateur https://localhost:8443
Architecture Docker PHP : conteneur web (PHP 8.5, Apache, Node.js, Composer) + MailHog pour les emails de test. Le code source est monté via un volume.

Le Dockerfile : construire l'image PHP

Le Dockerfile décrit précisément l'environnement serveur. C'est la documentation vivante de votre configuration — chaque extension PHP, chaque module Apache, chaque outil installé est versionné et reproductible.

FROM php:8.5-apache

# Extensions PHP nécessaires
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) \
    gd zip intl mbstring xml opcache

# Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Node.js via nvm (pour Webpack/Vite)
ENV NVM_DIR=/usr/local/nvm
RUN mkdir -p $NVM_DIR \
    && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh \
    | NVM_DIR=$NVM_DIR bash \
    && . "$NVM_DIR/nvm.sh" && nvm install 24

# Apache : activer les modules
RUN a2enmod rewrite headers expires deflate ssl

# SSL auto-signé (développement)
RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
    -keyout /etc/ssl/private/ssl-cert-snakeoil.key \
    -out /etc/ssl/certs/ssl-cert-snakeoil.pem \
    -subj "/CN=localhost"

WORKDIR /var/www/html
EXPOSE 80 443

Points importants :

  • Image officielle php:8.5-apache — Inclut Apache avec mod_php, pas besoin d'un conteneur Nginx séparé.
  • Extensions compilées — docker-php-ext-install gère la compilation. Pour Xdebug et Redis, utiliser pecl install.
  • Composer via multi-stage — COPY --from=composer:latest copie le binaire sans installer les dépendances de Composer.
  • Node.js via nvm — Nécessaire si le projet utilise Webpack ou Vite pour compiler les assets.

Docker Compose : orchestrer les services

# docker-compose.yml
services:
  web:
    build: .
    ports:
      - "8080:80"
      - "8443:443"
    volumes:
      - .:/var/www/html
    depends_on:
      - mailhog

  mailhog:
    image: mailhog/mailhog:latest
    ports:
      - "8025:8025"   # Interface web
      - "1025:1025"   # SMTP

Le volumes: .:/var/www/html monte le code source local dans le conteneur. Chaque modification de fichier est immédiatement visible — pas besoin de rebuild.

Services additionnels courants

  • MySQL / MariaDB — Ajouter un service db avec un volume persistant pour les données.
  • Redis — Cache objet pour les applications Symfony ou Laravel.
  • PhpMyAdmin — Interface web pour la base de données en développement.
  • Elasticsearch — Pour les boutiques PrestaShop ou Sylius avec recherche avancée.

Gestion des ports : éviter les conflits entre projets

Si vous travaillez sur plusieurs projets Docker en parallèle, le problème classique arrive vite : deux projets essaient d'utiliser le même port (8080, 3306, 8443...) et Docker refuse de démarrer le second avec l'erreur Bind for 0.0.0.0:8080 failed: port is already allocated.

Solution : ports configurables via .env

La bonne pratique est de ne jamais hardcoder les ports dans le docker-compose.yml mais d'utiliser des variables d'environnement avec des valeurs par défaut :

# docker-compose.yml
services:
  web:
    ports:
      - "${APP_PORT:-8080}:80"
      - "${APP_SSL_PORT:-8443}:443"
  db:
    ports:
      - "${DB_PORT:-3306}:3306"
  mailhog:
    ports:
      - "${MAILHOG_PORT:-8025}:8025"

Chaque projet a son propre fichier .env avec des ports uniques :

# Projet A (.env)
APP_PORT=8080
APP_SSL_PORT=8443
DB_PORT=3306

# Projet B (.env)
APP_PORT=8180
APP_SSL_PORT=8543
DB_PORT=3307

# Projet C (.env)
APP_PORT=8280
APP_SSL_PORT=8643
DB_PORT=3308

Convention de plages de ports par projet

Pour les équipes ou les développeurs qui jonglent entre plusieurs projets, attribuer une plage de ports dédiée à chaque projet évite les collisions :

  • Projet A : ports 8000-8099 (web 8080, SSL 8043, DB 8006, mail 8025)
  • Projet B : ports 8100-8199 (web 8180, SSL 8143, DB 8106, mail 8125)
  • Projet C : ports 8200-8299 (web 8280, SSL 8243, DB 8206, mail 8225)

Diagnostiquer un conflit de port

Si Docker refuse de démarrer, identifier quel processus occupe le port :

# Linux / macOS / WSL
sudo lsof -i :8080
sudo ss -tlnp | grep 8080

# Lister les conteneurs Docker et leurs ports
docker ps --format "{{.Names}}: {{.Ports}}" | grep 8080

Solution avancée : Traefik comme reverse proxy

Quand le nombre de projets grandit, gérer des plages de ports manuellement devient fastidieux. Traefik est un reverse proxy conçu pour Docker qui résout le problème élégamment : tous les projets utilisent le port 80/443 et Traefik route le trafic vers le bon conteneur en fonction du nom de domaine.

# docker-compose.yml (Traefik partagé entre tous les projets)
services:
  traefik:
    image: traefik:v3
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command:
      - --providers.docker=true
      - --entrypoints.web.address=:80

Chaque projet déclare son domaine via des labels Docker, sans exposer de port :

# docker-compose.yml (projet A)
services:
  web:
    labels:
      - "traefik.http.routers.projet-a.rule=Host(`projet-a.localhost`)"
    networks:
      - traefik

# docker-compose.yml (projet B)
services:
  web:
    labels:
      - "traefik.http.routers.projet-b.rule=Host(`projet-b.localhost`)"
    networks:
      - traefik

Résultat : http://projet-a.localhost et http://projet-b.localhost fonctionnent simultanément sur le même port 80, sans conflit. Traefik détecte automatiquement les conteneurs qui démarrent et les ajoute au routing. C'est la solution recommandée pour les développeurs qui travaillent sur 3+ projets simultanément.

Astuce : ne pas exposer les ports inutilement

Les conteneurs qui communiquent entre eux (web → db, web → mailhog) n'ont pas besoin de ports exposés sur l'hôte. Docker Compose crée un réseau interne où les services se contactent par leur nom (db:3306, mailhog:1025). N'exposez un port que si un humain a besoin d'y accéder (l'interface web MailHog, PhpMyAdmin, le site lui-même).

Le Makefile : simplifier le quotidien

Taper docker compose exec web bash -c "source /usr/local/nvm/nvm.sh && npm run prod" à chaque fois est fastidieux. Un Makefile encapsule les commandes Docker dans des raccourcis lisibles :

# Makefile
start: ## Premier lancement
    docker compose up -d --build
    docker compose exec -T web composer install
    docker compose exec -T web npm install
    docker compose exec -T web npm run prod

up:    ## Démarrer
    docker compose up -d

down:  ## Arrêter
    docker compose down

shell: ## Terminal dans le conteneur
    docker compose exec web bash

npm-prod: ## Compiler les assets
    docker compose exec web npm run prod

phpstan: ## Analyse statique
    docker compose exec web vendor/bin/phpstan analyse

test:  ## Tests PHPUnit
    docker compose exec web vendor/bin/phpunit

L'équipe n'a pas besoin de connaître Docker en détail — make start installe tout, make up démarre, make test lance les tests.

MailHog : tester les emails sans SMTP

MailHog intercepte tous les emails envoyés par l'application et les affiche dans une interface web à http://localhost:8025. L'application envoie sur le port SMTP 1025 au lieu du serveur de production — aucun email n'atteint de vraies boîtes aux lettres.

Configuration dans l'application PHP :

// Détection automatique de l'environnement Docker
if (file_exists('/.dockerenv')) {
    $transport = Transport::fromDsn('smtp://mailhog:1025');
} else {
    $transport = new SendmailTransport();
}

WSL2 sur Windows : configuration critique

Docker Desktop sur Windows utilise WSL2 (Windows Subsystem for Linux) comme moteur. La performance dépend de l'emplacement des fichiers :

  • Filesystem Linux (/home/user/projects/) — Performances natives, opérations fichiers rapides. C'est ici qu'il faut travailler.
  • Filesystem Windows monté (/mnt/c/Users/...) — 5 à 10 fois plus lent. Les npm install et composer install sont pénibles. À éviter.

Pour migrer un projet existant : copier le code dans le filesystem WSL (cp -r /mnt/c/www/monprojet ~/projects/) et travailler depuis là. VS Code supporte nativement WSL via l'extension "WSL" — l'éditeur s'ouvre sur Windows mais les fichiers sont dans Linux.

Bonnes pratiques Docker pour PHP

Images et Dockerfile

  • Versions explicites — php:8.5-apache au lieu de php:latest. Évite les surprises quand une nouvelle version sort.
  • Extensions pinnées — pecl install xdebug-3.3.2 au lieu de pecl install xdebug. Garantit la compatibilité.
  • Multi-stage builds — Séparer l'image dev (avec Xdebug, Node.js) de l'image prod (minimale). L'image prod est plus légère et plus sécurisée.
  • .dockerignore — Exclure vendor/, node_modules/, .git/ du contexte de build pour accélérer la construction.

Docker Compose

  • Variables d'environnement — Utiliser un fichier .env pour les ports et credentials. Ne pas hardcoder dans docker-compose.yml.
  • Volumes nommés pour les données persistantes (MySQL). Les données survivent à un docker compose down.
  • Healthcheck — Ajouter un healthcheck sur le conteneur web pour que Docker Compose sache quand le service est prêt.

Workflow quotidien

  • make up en début de journée — Démarre les conteneurs
  • Développer normalement — Les fichiers sont synchronisés en temps réel via le volume
  • make npm-dev si vous modifiez du SCSS/JS — Webpack watch dans le conteneur
  • make test avant chaque commit — PHPUnit dans le conteneur
  • make down en fin de journée — Arrête les conteneurs, libère les ressources

Intégration avec la CI/CD

L'environnement Docker local et la CI/CD partagent le même objectif : un environnement reproductible. La CI utilise les mêmes versions de PHP, les mêmes extensions et les mêmes outils que le Docker local. Si ça passe en local, ça passe en CI.

Le Makefile sert de pont : make phpstan exécute la même commande en local (via Docker) et en CI (directement sur le runner). La parité dev/CI réduit les "ça marche chez moi".

Questions fréquentes

Pourquoi utiliser Docker plutôt que WAMP/XAMPP/MAMP ?

Docker isole chaque projet dans son propre conteneur avec sa version de PHP, ses extensions et sa configuration. Fini les conflits entre projets qui nécessitent des versions PHP différentes. L'environnement est reproductible : un collègue qui clone le repo et lance docker compose up a exactement le même setup. Et le Dockerfile documente précisément la configuration serveur.

Docker est-il lent sur Windows ?

Docker sur Windows via WSL2 est performant à condition de travailler dans le filesystem Linux (/home/user/), pas dans le filesystem Windows monté (/mnt/c/). La différence de performance est significative — les opérations fichiers sont 5 à 10 fois plus rapides sur le filesystem WSL2 natif.

Faut-il Docker en production ?

Pas obligatoirement. Docker est un outil de développement avant tout. En production, un hébergement classique (Apache/PHP sur un serveur dédié ou mutualisé) fonctionne très bien pour un site PHP. Docker en production est pertinent pour les architectures microservices ou les déploiements Kubernetes, pas pour un site vitrine ou une boutique e-commerce standard.

Comment débugger avec Xdebug dans Docker ?

Installer Xdebug dans le Dockerfile (pecl install xdebug), configurer xdebug.mode=debug et xdebug.client_host=host.docker.internal (port 9003). Sur Linux, ajouter extra_hosts: host.docker.internal:host-gateway dans le docker-compose.yml. Configurer l'IDE (VS Code ou PhpStorm) pour écouter sur le port 9003.

Top