Symfony dans Docker : Dockerfile production-ready
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-authoritativeelimine 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 installsans--no-deven 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.
