Symfony et Docker en 2026 : environnement de développement et déploiement en production
Un workflow Symfony Docker complet pour 2026 : environnement de développement FrankenPHP, image de production multi-étapes, mode worker, secrets chiffrés, optimisation de l'image et déploiements sans interruption avec migrations.

Ce tutoriel Symfony Docker construit un workflow de conteneurs complet : un environnement de développement local reproductible et une image de production durcie, tous deux centrés sur FrankenPHP et Symfony 7.4 LTS. Exécuter Symfony dans des conteneurs supprime le classique décalage d'environnement entre les postes de développement et les serveurs, et transforme chaque déploiement en la livraison d'un unique artefact immuable, au lieu d'enchaîner une fragile série de commandes serveur manuelles.
Le template officiel Symfony Docker embarque désormais FrankenPHP comme runtime, en remplacement du duo traditionnel Nginx et PHP-FPM. Un seul binaire sert HTTP/2 et HTTP/3 et exécute Symfony en mode worker persistant, en gardant le kernel démarré entre les requêtes plutôt que de le reconstruire à chaque appel.
Environnement de développement Docker Compose pour Symfony
Un bon environnement de développement donne à chaque contributeur la même version de PHP, les mêmes extensions et la même base de données, sans jamais toucher à la machine hôte. Docker Compose décrit cette stack de manière déclarative. L'exemple ci-dessous associe un conteneur FrankenPHP construit à partir d'un Dockerfile local à un service PostgreSQL 17, reliés entre eux sur le réseau Compose par défaut.
# compose.yaml
services:
php:
build:
context: .
target: frankenphp_dev
ports:
- "80:80"
- "443:443"
volumes:
- ./:/app # bind mount: edits on the host appear instantly
- caddy_data:/data # persist TLS certificates across restarts
environment:
DATABASE_URL: "postgresql://app:app@database:5432/app?serverVersion=17"
depends_on:
- database
database:
image: postgres:17-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: app
volumes:
- db_data:/var/lib/postgresql/data
volumes:
caddy_data:
db_data:Le bind mount du service php fait correspondre la racine du projet à l'intérieur du conteneur : les changements de fichiers prennent effet sans reconstruire l'image. FrankenPHP génère un certificat TLS local au premier démarrage, d'où l'exposition du port 443 et le fait que caddy_data soit un volume nommé : le certificat survit à un docker compose down. L'indication serverVersion=17 dans la DSN permet à Doctrine d'éviter une requête de détection de version à chaque connexion.
Une fois la stack lancée, chaque commande Symfony s'exécute dans le conteneur php plutôt que sur l'hôte, ce qui garantit la bonne version de PHP et les bonnes extensions. Créer une base de données, lancer des migrations ou ouvrir un shell suivent tous le même schéma via docker compose exec.
Préfixez les appels Symfony et Composer par docker compose exec php pour les exécuter contre le runtime PHP du conteneur, par exemple docker compose exec php bin/console make:entity ou docker compose exec php composer require symfony/uid. L'hôte n'a jamais besoin d'avoir PHP installé.
Dockerfile multi-étapes pour la production Symfony
Un Dockerfile multi-étapes partage une base commune, puis se scinde en une cible de développement et une cible de production. L'étape de production n'installe que les dépendances d'exécution, écarte les paquets dev de Composer et préchauffe le cache au moment du build, si bien que le conteneur en fonctionnement démarre instantanément.
# Dockerfile
FROM dunglas/frankenphp:1-php8.4 AS base
WORKDIR /app
# Install the PHP extensions Symfony relies on
RUN install-php-extensions \
intl \
opcache \
pdo_pgsql \
zip
COPY /composer /usr/bin/composer
# --- Development target ---
FROM base AS frankenphp_dev
ENV APP_ENV=dev
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
# --- Production target ---
FROM base AS frankenphp_prod
ENV APP_ENV=prod
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
ENV APP_RUNTIME="Runtime\FrankenPhpSymfony\Runtime"
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
# Layer caching: copy manifests first, install, then copy the source
COPY composer.json composer.lock symfony.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
COPY . .
RUN composer dump-autoload --no-dev --classmap-authoritative \
&& composer dump-env prod \
&& bin/console cache:warmupCopier composer.json et les fichiers de lock avant le reste du code source est l'optimisation clé : Docker met en cache la couche d'installation des dépendances et ne la rejoue que lorsque les manifestes changent, pas à chaque modification de code. L'option --classmap-authoritative indique à Composer que la classmap est complète, si bien que l'autoloader ne se rabat jamais sur des recherches système de fichiers à l'exécution. Préchauffer le cache pendant le build signifie que la première requête de production frappe un conteneur entièrement compilé.
Mode worker FrankenPHP et le composant Runtime de Symfony
Le PHP-FPM traditionnel démarre le kernel Symfony, traite une requête, puis jette tout. Le mode worker garde le kernel et le conteneur d'injection de dépendances en vie d'une requête à l'autre, et c'est de là que provient l'essentiel du gain de latence. Le paquet runtime/frankenphp-symfony rend cela possible grâce au composant Symfony Runtime et au bootstrap standard public/index.php.
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};La ligne FRANKENPHP_CONFIG="worker ./public/index.php" définie dans le Dockerfile active cette boucle. Comme le conteneur est réutilisé, tout service qui conserve un état spécifique à une requête doit se réinitialiser entre les requêtes. Symfony gère automatiquement les services du framework, mais les services stateful personnalisés devraient implémenter Symfony\Contracts\Service\ResetInterface et être marqués avec le tag kernel.reset, afin que l'état accumulé ne fuite pas vers la requête suivante. C'est la même discipline qu'exigent les workers Messenger de longue durée, et la récompense est un débit de requêtes plusieurs fois supérieur à celui de PHP-FPM sur le même matériel.
En développement local, lancer FrankenPHP avec le flag --watch redémarre automatiquement le worker à chaque changement de fichier, de sorte que le mode worker ne gêne pas la boucle édition-rafraîchissement.
FrankenPHP face à PHP-FPM avec Symfony
Le choix entre FrankenPHP et la stack classique Nginx plus PHP-FPM façonne à la fois le Dockerfile et le comportement à l'exécution. Le tableau ci-dessous résume les différences concrètes pour un environnement de production Symfony.
| Aspect | Mode worker FrankenPHP | Nginx + PHP-FPM | |--------|------------------------|-----------------| | Conteneurs | Un seul | Deux (serveur web + PHP) | | Démarrage du kernel | Une fois par worker | Une fois par requête | | HTTP/3 | Intégré | Nécessite une configuration supplémentaire | | TLS | Automatique (Caddy) | Configuration manuelle du certificat | | Gestion de l'état | Doit se réinitialiser entre les requêtes | Isolé à chaque requête | | Coût du démarrage à froid | Payé une fois au boot | Payé à chaque requête |
Le mode worker l'emporte sur le débit parce qu'il amortit l'étape coûteuse de démarrage du kernel et de compilation du conteneur sur des milliers de requêtes. PHP-FPM reste pertinent quand une application s'appuie sur des variables globales ou des bibliothèques tierces qui supposent un processus neuf par requête, car celles-ci cassent sous un worker réutilisé.
Un service qui met en cache les données d'une requête dans une propriété privée servira les données d'une requête à la suivante s'il ne se réinitialise pas. Auditez les singletons, les event subscribers et tout ce qui détient un token de sécurité ou la requête courante avant de passer au mode worker en production.
Prêt à réussir tes entretiens Symfony ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Gérer les variables d'environnement et les secrets en production
Symfony lit sa configuration depuis les variables d'environnement, et il existe deux façons saines de les fournir en production. La première consiste à injecter des variables en clair via l'orchestrateur ou le fichier Compose. La seconde est le coffre-fort de secrets chiffrés de Symfony, qui stocke les valeurs sensibles dans le dépôt sous forme de texte chiffré et les déchiffre à l'exécution avec une unique clé privée.
# Generate the keypair; commit the public key, keep the private key out of the image
php bin/console secrets:generate-keys
# Encrypt a value into config/secrets/prod/
php bin/console secrets:set DATABASE_URL
# In production, expose only the decryption key to the container
export SYMFONY_DECRYPTION_SECRET="$(cat config/secrets/prod/prod.decrypt.private.php)"L'étape composer dump-env prod du Dockerfile compile les fichiers .env en un unique .env.local.php optimisé, ce qui supprime le coût d'exécution lié au parsing des fichiers dotenv au démarrage. Toute variable définie dans l'environnement réel du conteneur écrase malgré tout les valeurs par défaut compilées : les secrets fournis par l'orchestrateur l'emportent toujours. Le guide de déploiement Symfony documente l'ordre de précédence complet, mais la règle pratique est simple : ne jamais figer APP_SECRET ni les identifiants de base de données dans l'image, et les injecter au démarrage du conteneur.
Optimiser l'image de production Symfony
Deux réglages OPcache expliquent l'essentiel de l'écart de performance en production : la désactivation de la validation des timestamps et l'activation du preloading. Comme une image de conteneur est immuable, le code source ne change jamais à l'exécution, donc OPcache ne devrait jamais faire de stat sur les fichiers pour vérifier d'éventuelles modifications. Le preloading va plus loin en chargeant les classes de Symfony et de l'application en mémoire partagée, une seule fois au démarrage du serveur.
; docker/php/prod.ini
opcache.enable=1
opcache.preload=/app/var/cache/prod/App_KernelProdContainer.preload.php
opcache.preload_user=root
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0 ; immutable image: never re-check the filesystem
realpath_cache_size=4096K
realpath_cache_ttl=600Symfony génère le fichier preload.php listé ci-dessus lors du cache:warmup, si bien que le fichier existe déjà dans l'image après le build. Le preloading OPcache lie ces classes au démarrage et évite l'étape de compilation lors de la première requête qui les nécessite. Régler validate_timestamps=0 n'est sûr que pour des déploiements immuables, où une nouvelle version signifie une nouvelle image ; sur un hôte mutable, cela servirait du code périmé. Dimensionner max_accelerated_files au-dessus du nombre réel de fichiers garde tout le framework en cache sans éviction.
Réduire la taille de l'image Symfony avec .dockerignore
Un fichier .dockerignore absent gonfle silencieusement le build. Sans lui, l'étape COPY . . embarque le répertoire vendor local, le var/cache de développement, la sortie de build node et l'historique .git directement dans l'image et le contexte de build. Les exclure réduit l'image, accélère l'envoi du contexte de build vers le daemon et empêche les artefacts de développement d'écraser les dépendances de production fraîchement installées.
# .dockerignore
/.git/
/vendor/
/node_modules/
/var/
/.env.local
/.env.*.local
/tests/
/docker/
compose*.yaml
DockerfileIgnorer /vendor/ compte le plus : l'étape de production exécute composer install --no-dev, donc embarquer le répertoire vendor à saveur développement de l'hôte annulerait complètement cette étape. Exclure /var/ garde le cache et les logs de développement hors de l'image, ce qui laisse le cache:warmup du build produire un cache de production propre. Combinée au build multi-étapes et aux extensions basées sur Alpine, une image Symfony épurée passe généralement bien sous les 150 Mo.
Déployer les conteneurs Symfony et lancer les migrations
Le fichier Compose de production référence une image pré-construite issue d'un registre plutôt que de la construire localement, et il définit un health check pour que l'orchestrateur ne route le trafic que vers un conteneur qui répond. Les secrets arrivent comme variables d'environnement, jamais comme couches d'image.
# compose.prod.yaml
services:
php:
image: registry.example.com/app:${TAG}
environment:
APP_ENV: prod
APP_SECRET: ${APP_SECRET}
DATABASE_URL: ${DATABASE_URL}
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost/health"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stoppedLe health check interroge une route /health via curl, donc l'application doit en exposer une. Un contrôleur minimal qui renvoie un 200 sans toucher à la base de données garde la vérification rapide et évite de marquer le conteneur comme non sain lors d'un incident transitoire de base de données. Quand la disponibilité de la base compte vraiment, une seconde sonde plus profonde peut exécuter une requête légère, mais l'endpoint de liveness reste trivial.
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
final class HealthController extends AbstractController
{
#[Route('/health', name: 'health', methods: ['GET'])]
public function __invoke(): JsonResponse
{
return new JsonResponse(['status' => 'ok']);
}
}Les migrations de base de données devraient s'exécuter comme une étape distincte avant que les nouveaux conteneurs ne prennent le trafic, et non à l'intérieur du boot de l'application. Les lancer dans un conteneur éphémère garantit que le schéma est à jour exactement une fois par release, même quand plusieurs répliques de l'application démarrent en parallèle.
# deploy.sh
set -euo pipefail
docker compose -f compose.prod.yaml pull
# Apply migrations once, in an isolated container
docker compose -f compose.prod.yaml run --rm php \
bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
# Start replicas and wait for the health check to pass
docker compose -f compose.prod.yaml up -d --waitLe flag --wait bloque jusqu'à ce que chaque service se déclare sain, ce qui donne au script de déploiement un vrai signal de succès plutôt qu'un démarrage lancé à l'aveugle. Combiner cela à une mise à jour progressive dans un orchestrateur comme Kubernetes ou Docker Swarm produit des releases sans interruption : les anciens conteneurs continuent de servir jusqu'à ce que les nouveaux passent leur health check. Pour une vue d'ensemble des versions du framework que vise ce workflow, la présentation de la plateforme Symfony et le guide des nouveautés de Symfony 8 et PHP 8.4 détaillent ce qui a changé dans la ligne de version actuelle.
Conclusion
- Utilisez un Dockerfile multi-étapes pour que les images de développement et de production partagent une base unique, pendant que la cible de production se livre sans les dépendances dev de Composer
- Copiez
composer.jsonet les fichiers de lock avant le code source pour garder la couche d'installation des dépendances en cache d'une modification de code à l'autre - Exécutez FrankenPHP en mode worker pour réutiliser le kernel démarré d'une requête à l'autre, et réinitialisez les services stateful avec
ResetInterfaceafin de prévenir les fuites d'état - Préchauffez le cache Symfony et compilez
.envavecdump-env prodau moment du build, pour que le conteneur en fonctionnement démarre entièrement initialisé - Activez le preloading OPcache et réglez
validate_timestamps=0en production, ce qui est sûr précisément parce que l'image est immuable - Gardez
APP_SECRET, les identifiants de base de données et la clé de déchiffrement du coffre hors des couches d'image ; injectez-les comme variables d'environnement au démarrage du conteneur - Appliquez les migrations Doctrine dans un conteneur éphémère avant que les nouvelles répliques ne prennent le trafic, et conditionnez le déploiement au health check avec
up -d --wait
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

Doctrine ORM : Maîtriser les relations en Symfony
Guide complet des relations Doctrine ORM dans Symfony. OneToMany, ManyToMany, stratégies de chargement et optimisation des performances avec exemples pratiques.

Questions d'entretien Symfony : Top 25 en 2026
Les 25 questions d'entretien Symfony les plus posées. Architecture, Doctrine ORM, services, sécurité, formulaires et tests avec réponses détaillées et exemples de code.

Symfony 7 : API Platform et bonnes pratiques
Guide complet pour créer des APIs REST professionnelles avec Symfony 7 et API Platform 4. State Providers, Processors, validation et sérialisation expliqués.