◀ Retour au blog
PHP / Symfony

Développement Symfony avec Docker

Publié le 22 Oct 2024· 7 min de lecture
#Symfony#Docker#Développement

Symfony et Docker : le duo parfait

Docker et Symfony forment une combinaison puissante pour le développement. Chez Keytchens, plateforme food-tech de gestion de commandes en temps réel, cette stack a permis d'unifier les environnements de développement pour toute l'équipe.

Ce guide décrit un environnement de développement complet : PHP-FPM, Nginx, MySQL, Redis et RabbitMQ, pilotés par Docker Compose et un Makefile. L'objectif est simple : un nouveau développeur clone le dépôt, lance une commande, et obtient une application fonctionnelle, identique à celle de ses collègues.

Pourquoi conteneuriser l'environnement de développement

Sans conteneurs, chaque poste accumule sa propre version de PHP, ses extensions, son serveur MySQL et ses réglages. Les écarts finissent toujours par produire le fameux « ça marche chez moi ». Docker règle ce problème en décrivant l'environnement sous forme de code versionné avec l'application :

  • la version de PHP et la liste des extensions sont fixées dans un Dockerfile ;
  • les versions de MySQL, Redis et RabbitMQ sont celles de la production, pas celles installées par hasard sur le poste ;
  • plusieurs projets aux besoins incompatibles (PHP 7.4 et PHP 8.3, par exemple) cohabitent sans conflit ;
  • une mise à jour de l'environnement passe par une pull request, relue comme n'importe quel code.

Docker Compose pour Symfony

Le fichier compose.yaml (ou docker-compose.yml) déclare l'ensemble des services :

services:
  php:
    build: .docker/php
    volumes:
      - .:/var/www/html
    depends_on:
      - mysql
      - redis

  nginx:
    image: nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - .:/var/www/html
      - .docker/nginx/default.conf:/etc/nginx/conf.d/default.conf

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: symfony
    volumes:
      - mysql-data:/var/lib/mysql

  redis:
    image: redis:7-alpine

  rabbitmq:
    image: rabbitmq:3-management-alpine
    ports:
      - "15672:15672"

volumes:
  mysql-data:

Quelques points à comprendre dans ce fichier :

  • Le bind mount .:/var/www/html monte le code source du poste dans les conteneurs php et nginx : toute modification est visible immédiatement, sans reconstruire l'image.
  • Le volume nommé mysql-data conserve les données de la base entre deux docker compose down. Seul docker compose down -v le supprime.
  • Le réseau est créé automatiquement : chaque service est joignable par son nom (mysql, redis, rabbitmq). Depuis Symfony, on n'utilise donc jamais localhost pour atteindre la base.
  • Le port 15672 expose l'interface d'administration de RabbitMQ sur http://localhost:15672 (identifiants par défaut guest / guest).

Attendre que la base soit réellement prête

depends_on sous sa forme simple ne garantit que l'ordre de démarrage, pas la disponibilité : MySQL peut encore être en cours d'initialisation quand PHP essaie de s'y connecter. Un healthcheck combiné à la condition service_healthy règle ce problème :

services:
  php:
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_started

  mysql:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-proot"]
      interval: 5s
      timeout: 3s
      retries: 10

Configuration Nginx

Le fichier .docker/nginx/default.conf reprend la configuration recommandée par la documentation Symfony. Nginx sert les fichiers statiques du dossier public/ et transmet tout le reste à PHP-FPM, joignable sous le nom php sur le port 9000 :

server {
    listen 80;
    server_name localhost;
    root /var/www/html/public;

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

    location ~ ^/index\.php(/|$) {
        fastcgi_pass php:9000;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        internal;
    }

    location ~ \.php$ {
        return 404;
    }
}

Le dernier bloc renvoie une erreur 404 pour tout autre fichier PHP : seul le contrôleur frontal index.php doit être exécutable.

Dockerfile PHP optimisé

FROM php:8.3-fpm
RUN apt-get update && apt-get install -y \
    libicu-dev libzip-dev \
    && docker-php-ext-install intl pdo_mysql zip opcache \
    && pecl install redis xdebug \
    && docker-php-ext-enable redis xdebug

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html

L'image officielle php:8.3-fpm fournit les scripts docker-php-ext-install et docker-php-ext-enable. Les bibliothèques système (libicu-dev pour intl, libzip-dev pour zip) doivent être installées avant de compiler les extensions. Les extensions PECL comme redis et xdebug se compilent avec pecl install, puis s'activent. Enfin, COPY --from=composer:2 récupère le binaire Composer depuis son image officielle, sans script d'installation.

Si vous utilisez le transport AMQP de Messenger avec RabbitMQ, il faut aussi l'extension amqp : ajoutez librabbitmq-dev aux paquets et amqp à la ligne pecl install.

Xdebug ralentit sensiblement chaque requête. Désactivez-le par défaut et activez-le seulement quand vous en avez besoin, via un fichier .docker/php/xdebug.ini copié dans l'image :

xdebug.mode=off
xdebug.client_host=host.docker.internal
xdebug.start_with_request=trigger

La variable d'environnement XDEBUG_MODE=debug, passée au conteneur, prend le pas sur xdebug.mode : vous activez le débogage sans reconstruire l'image. Sous Linux, host.docker.internal n'existe pas par défaut : ajoutez extra_hosts: ["host.docker.internal:host-gateway"] au service php.

Connecter Symfony aux services

Dans .env.local (non versionné), les DSN pointent vers les noms des services Compose :

DATABASE_URL="mysql://root:root@mysql:3306/symfony?serverVersion=8.0&charset=utf8mb4"
REDIS_URL="redis://redis:6379"
MESSENGER_TRANSPORT_DSN="amqp://guest:guest@rabbitmq:5672/%2f/messages"

Pensez à renseigner serverVersion : Doctrine l'utilise pour choisir la bonne plateforme SQL sans interroger le serveur au démarrage.

Makefile pour simplifier les commandes

Les commandes Docker sont longues et faciles à oublier. Un Makefile à la racine du projet donne à toute l'équipe le même vocabulaire :

up:
	docker compose up -d

down:
	docker compose down

console:
	docker compose exec php php bin/console $(cmd)

test:
	docker compose exec php php bin/phpunit

migrate:
	docker compose exec php php bin/console doctrine:migrations:migrate -n

Usage : make up, puis make console cmd="cache:clear" ou make migrate. Attention, les recettes d'un Makefile doivent être indentées par une tabulation, pas par des espaces. Ajoutez aussi une ligne .PHONY: up down console test migrate pour que make ne confonde pas ces cibles avec des fichiers portant le même nom.

Pièges courants

  • Permissions des fichiers : PHP-FPM tourne sous www-data dans le conteneur, alors que vos fichiers appartiennent à votre utilisateur. Si var/cache ou var/log ne sont pas inscriptibles, alignez l'UID de l'utilisateur du conteneur sur le vôtre (argument de build UID et usermod) plutôt que de faire un chmod 777.
  • Lenteur sur macOS : les bind mounts y sont plus lents que sous Linux. Activez VirtioFS dans Docker Desktop et évitez de monter des dossiers inutiles.
  • Composer hors du conteneur : lancez composer install dans le conteneur, sinon les dépendances seront résolues pour la version de PHP du poste, pas celle de l'image.
  • Mots de passe en clair : root / root est acceptable en local uniquement. Ne réutilisez jamais ce fichier Compose tel quel en production.
  • Image de production : cette image embarque Xdebug et monte le code en volume. Pour la production, construisez une image distincte (build multi-stage) avec le code copié, composer install --no-dev et OPcache configuré sans vérification des timestamps.

Les bénéfices au quotidien

  • Environnement identique pour tous les développeurs
  • Isolation complète des services
  • Facilité d'onboarding pour les nouveaux développeurs
  • Versions des services alignées sur la production
  • Changements d'environnement relus et versionnés comme le code

Checklist de démarrage

  1. Écrire le compose.yaml, le Dockerfile PHP et la configuration Nginx.
  2. Ajouter un healthcheck sur MySQL et conditionner le démarrage de PHP.
  3. Désactiver Xdebug par défaut et l'activer à la demande.
  4. Renseigner les DSN dans .env.local avec les noms des services.
  5. Documenter les commandes dans un Makefile et dans le README.
  6. Vérifier qu'un git clone suivi de make up suffit à démarrer le projet sur un poste vierge.

Avec cette base, l'environnement de développement devient un élément du projet à part entière : reproductible, documenté et facile à faire évoluer.