Symfony14 min

Symfony dans Docker : Dockerfile production-ready

Par Pierre-Arthur Demengel
SymfonyDockerDevOpsProduction

Deployer Symfony en production sans Docker en 2026, c'est comme deployer sans controle de version : techniquement possible, mais dangereusement fragile. Docker garantit que votre application tourne dans le meme environnement du laptop au serveur. Voici comment construire un Dockerfile production-ready pour Symfony 7.2 avec PHP 8.3, etape par etape.

Pourquoi Docker pour Symfony ?

Les arguments classiques restent valides : reproductibilite, isolation, scalabilite horizontale. Mais pour Symfony specifiquement, Docker resout trois problemes concrets. D'abord, la gestion des extensions PHP - plus besoin de negocier avec l'admin sys pour installer intl ou amqp. Ensuite, la parite dev/prod - fini les "ca marche sur ma machine" causes par une version de PHP ou de PostgreSQL differente. Enfin, le deploiement atomique - une image Docker est immutable, vous ne modifiez jamais un serveur en place.

Le projet symfony-docker de Kevin Dunglas (co-createur d'API Platform) fournit une base excellente. Ce guide vous explique chaque couche pour que vous compreniez ce que vous deployez, pas juste copier-coller un Dockerfile mystique.

Le Dockerfile multi-stage

Un Dockerfile multi-stage separe les phases de build et de runtime. Le stage de build installe Composer, les dependances, compile les assets. Le stage de production ne copie que le resultat final. Resultat : une image legere et securisee.

# Stage 1 : Dependances Composer
FROM php:8.3-fpm-alpine AS builder

RUN apk add --no-cache \
    icu-dev \
    libzip-dev \
    linux-headers \
    $PHPIZE_DEPS

RUN docker-php-ext-install \
    intl \
    opcache \
    pdo_pgsql \
    zip

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

WORKDIR /app

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

COPY . .
RUN composer dump-autoload --classmap-authoritative \
    && composer run-script post-install-cmd 2>/dev/null || true

# Stage 2 : Image de production
FROM php:8.3-fpm-alpine AS production

RUN apk add --no-cache \
    icu-libs \
    libzip \
    nginx \
    supervisor

RUN docker-php-ext-install \
    intl \
    opcache \
    pdo_pgsql \
    zip

# Configuration OPcache pour la production
RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/opcache.ini \
    && echo "opcache.memory_consumption=256" >> /usr/local/etc/php/conf.d/opcache.ini \
    && echo "opcache.max_accelerated_files=20000" >> /usr/local/etc/php/conf.d/opcache.ini \
    && echo "opcache.validate_timestamps=0" >> /usr/local/etc/php/conf.d/opcache.ini \
    && echo "opcache.preload_user=www-data" >> /usr/local/etc/php/conf.d/opcache.ini \
    && echo "opcache.preload=/app/config/preload.php" >> /usr/local/etc/php/conf.d/opcache.ini

WORKDIR /app

COPY --from=builder /app /app

RUN mkdir -p var/cache var/log \
    && chown -R www-data:www-data var

COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf

EXPOSE 80

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
    CMD curl -f http://localhost/health || exit 1

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

Points cles de ce Dockerfile. Le stage builder installe les devDependencies de Composer et les outils de build qui ne seront pas dans l'image finale. L'option opcache.validate_timestamps=0 desactive la verification des fichiers a chaque requete - essentiel en production ou le code ne change jamais dans un conteneur. Le preload Symfony charge les classes les plus utilisées en memoire au demarrage de PHP-FPM.

Configuration nginx

Le fichier nginx sert de reverse proxy vers PHP-FPM via un socket Unix :

server {
    listen 80;
    server_name _;
    root /app/public;

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

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

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

La directive internal sur le bloc PHP empeche l'acces direct aux fichiers PHP autres que index.php. C'est une couche de securite importante souvent oubliee.

docker-compose.yml pour le developpement

En developpement, vous voulez le rechargement a chaud, xdebug et l'acces aux logs. Voici un docker-compose adapte :

# docker-compose.yml (developpement)
services:
  app:
    build:
      context: .
      target: builder
    volumes:
      - .:/app
      - /app/vendor
    ports:
      - "8080:80"
    environment:
      APP_ENV: dev
      APP_DEBUG: 1
      DATABASE_URL: "postgresql://app:secret@db:5432/app?serverVersion=16"
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  db_data:

docker-compose.prod.yml

En production, pas de volumes montes (l'image est immutable), pas de debug, et les secrets sont injectes proprement :

# docker-compose.prod.yml
services:
  app:
    image: registry.example.com/mon-app-symfony:latest
    restart: unless-stopped
    environment:
      APP_ENV: prod
      APP_DEBUG: 0
    env_file:
      - .env.prod.local
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"

Le fichier .env.prod.local contient les secrets (DATABASE_URL, MAILER_DSN, etc.) et n'est jamais commite dans Git. Sur un serveur, il est deploye separement ou injecte par l'outil de CI/CD.

Gestion des secrets

Symfony propose son propre systeme de secrets via php bin/console secrets:set. Les secrets sont chiffres et commites dans config/secrets/prod/. Seule la cle de dechiffrement (prod.decrypt.private.php) doit rester hors du depot. Dans Docker, vous l'injectez via une variable d'environnement :

# Generer les secrets
php bin/console secrets:set DATABASE_URL --env=prod
php bin/console secrets:set MAILER_DSN --env=prod

# Dans le Dockerfile, la cle de dechiffrement vient de l'environnement
# Ne JAMAIS la mettre dans le Dockerfile
ENV SYMFONY_DECRYPTION_SECRET=""

Optimiser la taille de l'image

Quelques techniques pour reduire l'image finale :

  • Alpine - l'image de base fait environ 5 Mo contre 140 Mo pour Debian
  • .dockerignore - excluez .git, tests/, docker/, node_modules/, .env.local
  • --no-dev - Composer n'installe pas PHPUnit, Faker, les outils de debug
  • Classmap authoritative - composer dump-autoload --classmap-authoritative elimine les lookups fichier
# .dockerignore
.git
.github
docker-compose*.yml
Dockerfile
tests/
node_modules/
.env.local
.env.*.local
var/
*.md

Avec ces optimisations, une image Symfony typique pese entre 80 et 120 Mo. C'est raisonnable pour un demarrage rapide et un pull efficace.

Health checks et monitoring

Le health check dans le Dockerfile est un minimum. En production, creez un endpoint dedie dans Symfony :

// src/Controller/HealthController.php
namespace App\Controller;

use Doctrine\DBAL\Connection;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

class HealthController
{
    #[Route('/health', name: 'health_check', methods: ['GET'])]
    public function __invoke(Connection $connection): JsonResponse
    {
        try {
            $connection->executeQuery('SELECT 1');
            $dbStatus = 'ok';
        } catch (\Throwable) {
            $dbStatus = 'error';
        }

        return new JsonResponse([
            'status' => $dbStatus === 'ok' ? 'healthy' : 'degraded',
            'database' => $dbStatus,
            'timestamp' => time(),
        ], $dbStatus === 'ok' ? 200 : 503);
    }
}

Ce endpoint verifie la connexion a la base de donnees et retourne un code 503 si elle est indisponible. Docker, Kubernetes ou votre load balancer utiliseront ce signal pour retirer les conteneurs defaillants du pool.

Pipeline CI/CD pour le build

Un exemple de GitHub Actions pour builder et pousser l'image :

# .github/workflows/docker-build.yml
name: Build & Push Docker Image
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        with:
          context: .
          target: production
          push: true
          tags: ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Le cache GitHub Actions (type=gha) accelere considerablement les builds suivants en reutilisant les layers inchanges.

Erreurs courantes a eviter

Apres avoir accompagne plusieurs projets Symfony dans leur conteneurisation, voici les erreurs que je vois le plus souvent :

  • Lancer composer install sans --no-dev en production - vous embarquez PHPUnit et les outils de debug dans l'image
  • Oublier opcache.validate_timestamps=0 - PHP verifie les fichiers a chaque requete, ce qui est inutile dans un conteneur immutable
  • Monter des volumes en production - cela casse l'immutabilite et peut causer des problemes de permissions
  • Stocker des secrets dans le Dockerfile - ils sont visibles dans l'historique des layers
  • Ne pas avoir de health check - votre orchestrateur ne sait pas si le conteneur est fonctionnel

Aller plus loin

Une fois votre image Docker en place, consultez le guide Deployer une application Symfony : checklist 2026 pour la mise en production complete. Si vous partez de zero, le guide d'installation Symfony couvre la mise en place initiale du projet. Et pour optimiser les performances de votre application conteneurisee, le guide sur le cache Symfony vous aidera a configurer les couches de cache adaptees a Docker.

Besoin d'aide pour conteneuriser votre application Symfony ? Consultez mes tarifs ou services, et contactez-moi pour un accompagnement sur mesure. En tant que developpeur freelance base en France et en Belgique, j'accompagne des equipes dans la mise en place d'infrastructures Docker robustes pour Symfony.

Questions fréquentes

13 projets livrésGrand-Est & BelgiqueLighthouse >90Disponible immédiatement

Un projet en tête ?

Discutons de votre site web. Réponse garantie sous 24h.

Ou appelez directement :06 95 41 30 25

WhatsApp
Appeler