Aller au contenu principal
Retour au blog

Docker Compose Symfony : override files et profils, zéro duplication

Flavien Métivier25 mars 20258 min

Trois Dockerfile différents. Un docker-compose.yml pour le dev, une copie pour les tests, une variante bricolée pour la CI GitHub Actions. Six mois plus tard, une variable d'environnement change côté pipeline, personne ne la répercute en local, et les bugs de parité s'accumulent silencieusement — jusqu'au jour où reproduire un échec CI sur sa machine devient un sport de combat. Depuis le 6 mars 2025, Docker Compose v2 est actif par défaut dans Docker Desktop 4.39, sans configuration manuelle. C'est le bon moment pour remettre de l'ordre une bonne fois pour toutes. Voici l'architecture que j'utilise sur mes projets Symfony 7.2 / PHP 8.4 : un fichier de base, des override files ciblés, des profils pour les services optionnels comme Mailpit, et une CI GitHub Actions qui joue exactement les mêmes cartes que ta machine locale. Zéro duplication.

Le piège des environnements divergents

Le schéma classique : tu crées un docker-compose.yml en développement avec FrankenPHP, Xdebug et Mailpit. Quand vient le moment d'écrire les tests d'intégration, tu en fais une copie — tu retires Mailpit, tu changes le nom de la base. Puis la CI réclame une troisième variante parce que les ports diffèrent ou que les secrets sont injectés par le runner. Quelques sprints plus tard, les trois fichiers ont divergé. Une migration Doctrine tourne bien en test mais échoue en CI parce que la version de PostgreSQL diffère d'un cran. Un e-mail de notification ne part plus en dev parce que MAILER_DSN a été modifié dans un seul fichier. La solution n'est pas un monolithe de configuration : c'est d'utiliser les mécanismes que Docker Compose propose nativement — les override files et les profils.

Un docker-compose.yml de base : le contrat partagé

Le fichier racine docker-compose.yml définit la topologie de l'application sans opinion sur l'environnement d'exécution : services, contexte de build, healthchecks, variables communes. Aucun port exposé côté hôte, aucun volume de développement, aucune configuration Xdebug. La target de build par défaut pointe vers app — la cible de production. Les overrides surchargeront ce qui doit l'être. Versionné dans le dépôt, ce fichier est la référence incontestable partagée par tous les environnements.

# docker-compose.yml — contrat partagé entre tous les environnements
services:
  php:
    build:
      context: .
      target: app
    environment:
      APP_ENV: ${APP_ENV:-prod}
      APP_SECRET: ${APP_SECRET}
      DATABASE_URL: postgresql://app:${POSTGRES_PASSWORD:-app}@db:5432/app?serverVersion=16&charset=utf8
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-app}
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 5s
      retries: 5

Remarque l'absence de volume sur db dans ce fichier de base — c'est intentionnel. L'override dev ajoutera un volume persistant ; l'override test utilisera tmpfs pour une base en RAM. Le Dockerfile suit la même logique multi-stage : une cible base commune, une cible dev avec Xdebug, une cible app avec l'autoloader optimisé pour la production et la CI.

# Dockerfile
FROM dunglas/frankenphp:1-php8.4 AS base

ENV COMPOSER_ALLOW_SUPERUSER=1 \
    SERVER_NAME=":80"

WORKDIR /app

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

RUN install-php-extensions \
    apcu \
    intl \
    opcache \
    pdo_pgsql \
    zip

COPY composer.json composer.lock symfony.lock* ./
RUN composer install --no-dev --no-scripts --no-interaction --prefer-dist --no-progress

# Target développement : Xdebug activé, sources montées en volume
FROM base AS dev

RUN install-php-extensions xdebug

COPY . .

# Target production et CI : sources copiées, autoloader optimisé
FROM base AS app

COPY . .

RUN composer dump-autoload --classmap-authoritative --no-dev

L'override dev : FrankenPHP, Xdebug et volumes locaux

Docker Compose charge automatiquement docker-compose.override.yml quand il est présent dans le répertoire courant — sans passer -f manuellement. C'est dans ce fichier que tout ce qui est spécifique au développement prend place : la target dev (avec Xdebug), les ports exposés côté hôte pour accéder à l'application et à PostgreSQL depuis l'IDE, les volumes pour le hot reload des sources, les volumes Caddy pour les certificats locaux, et la variable MAILER_DSN qui redirige tous les e-mails vers Mailpit. Un simple docker compose up -d suffit pour retrouver l'environnement complet.

# docker-compose.override.yml — dev uniquement (chargé automatiquement)
services:
  php:
    build:
      target: dev                              # active la target avec Xdebug
    environment:
      APP_ENV: dev
      APP_SECRET: dev-secret-not-for-prod
      XDEBUG_MODE: ${XDEBUG_MODE:-debug}
      XDEBUG_CONFIG: client_host=host.docker.internal
      MAILER_DSN: smtp://mailpit:1025
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - .:/app                                 # hot reload : les modifications locales sont visibles immédiatement
      - caddy_data:/data
      - caddy_config:/config

  db:
    ports:
      - "5432:5432"                            # accès direct depuis l'IDE (DataGrip, TablePlus…)
    volumes:
      - db_data:/var/lib/postgresql/data       # persistance entre les redémarrages

  mailpit:
    profiles:
      - mail
    image: axllent/mailpit:v1
    ports:
      - "1025:1025"
      - "8025:8025"

volumes:
  caddy_data:
  caddy_config:
  db_data:

Mailpit et les profils Docker Compose : services à la carte

Besoin d'un expert Symfony ?

Réserver un appel

Les profils Docker Compose permettent de déclarer des services optionnels sans les démarrer par défaut. Un service portant un champ profiles est invisible pour docker compose up tant qu'il n'est pas activé explicitement avec --profile. C'est idéal pour Mailpit : certains développeurs travaillent sur des fonctionnalités sans e-mail, d'autres en ont besoin en permanence. Le service est configuré une seule fois dans l'override, activé à la demande. Dans le fichier .env.local Symfony, MAILER_DSN=smtp://mailpit:1025 — Mailpit intercepte tout et expose son interface web sur http://localhost:8025.

# démarrer l'environnement de dev sans Mailpit
docker compose up -d

# démarrer avec Mailpit activé
docker compose --profile mail up -d

# accéder à l'interface web de Mailpit
open http://localhost:8025

# arrêter tous les services (y compris les profils actifs)
docker compose --profile mail down

L'override test : PostgreSQL en RAM, PHPUnit isolé

Pour les tests, docker-compose.override.yml ne s'applique pas — il n'est chargé que lorsqu'aucun flag -f n'est passé explicitement. En combinant docker-compose.yml et docker-compose.test.yml avec deux flags -f, on obtient un environnement minimal et déterministe. La target dev embarque PHPUnit. La variable APP_ENV=test active le kernel Symfony de test. La base de données porte un nom dédié (app_test) et tourne entièrement en RAM grâce à tmpfs — même moteur PostgreSQL qu'en production, vitesse d'une base en mémoire, aucun effet de bord entre les runs, aucune donnée de dev qui risque de polluer les assertions. Puisque le fichier de base ne monte aucun volume sur db, l'option tmpfs s'applique proprement, sans conflit.

# docker-compose.test.yml — utilisé explicitement avec -f
services:
  php:
    build:
      target: dev                              # PHPUnit et Xdebug disponibles
    environment:
      APP_ENV: test
      APP_SECRET: test-secret
      DATABASE_URL: postgresql://app:app@db:5432/app_test?serverVersion=16&charset=utf8
    volumes:
      - .:/app

  db:
    environment:
      POSTGRES_DB: app_test                   # base dédiée aux tests
    tmpfs:
      - /var/lib/postgresql/data              # en RAM : rapide, sans résidu entre les runs

GitHub Actions : CI propre avec les mêmes fichiers qu'en local

En CI, la même logique s'applique : on passe -f docker-compose.yml -f docker-compose.test.yml à chaque commande docker compose. Pas de docker-compose.ci.yml à maintenir en parallèle, pas de variables secrètes sur la structure des services. Le flag --wait sur docker compose up garantit que PostgreSQL est sain — healthcheck passé — avant de lancer les migrations. C'est une feature disponible nativement depuis Docker Compose v2.1, bien présente dans les runners GitHub en mars 2025. Si les tests passent en local avec cette commande, ils passent en CI — par construction.

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Build images
        run: docker compose -f docker-compose.yml -f docker-compose.test.yml build --pull

      - name: Start database (attend le healthcheck)
        run: docker compose -f docker-compose.yml -f docker-compose.test.yml up -d --wait db

      - name: Run migrations
        run: |
          docker compose -f docker-compose.yml -f docker-compose.test.yml run --rm php \
            php bin/console doctrine:migrations:migrate --no-interaction

      - name: Run PHPUnit
        run: |
          docker compose -f docker-compose.yml -f docker-compose.test.yml run --rm php \
            php bin/phpunit --testdox

      - name: Teardown
        if: always()
        run: docker compose -f docker-compose.yml -f docker-compose.test.yml down -v

Les commandes du quotidien

  • Démarrer le dev (sans Mailpit) : docker compose up -d
  • Démarrer avec Mailpit : docker compose --profile mail up -d
  • Lancer PHPUnit en local (identique à la CI) : docker compose -f docker-compose.yml -f docker-compose.test.yml run --rm php php bin/phpunit
  • Appliquer les migrations de test : docker compose -f docker-compose.yml -f docker-compose.test.yml run --rm php php bin/console doctrine:migrations:migrate --no-interaction
  • Ouvrir un shell dans le conteneur dev : docker compose exec php bash
  • Rebuild après un changement dans le Dockerfile : docker compose build --no-cache php
  • Tout stopper et nettoyer les volumes : docker compose down -v

Trois fichiers, zéro duplication, environnements cohérents du laptop à la CI. La prochaine fois qu'un développeur rejoint le projet, un git clone suivi de docker compose --profile mail up -d suffit — aucune doc d'installation à maintenir. Si ton projet Symfony traîne encore des fichiers docker-compose hérités, plusieurs Dockerfile qui divergent ou une CI qui se comporte différemment de la machine locale, un Bear Scan permet de poser un diagnostic précis sur l'infrastructure de développement et de définir une roadmap de remise en ordre, sans tout recasser d'un coup.

Cet article vous a plu ? Partagez-le !

Besoin d'un expert Symfony ?

20 ans d'expérience sur l'écosystème PHP/Symfony.