Déploiements Symfony en production : workflow zéro downtime
13 octobre 20267 min de lecturePar Slim Sayari
Déploiements Symfony en production : workflow zéro downtime
Un git pull en production sur une application Symfony laisse une fenêtre d'incohérence : entre le moment où les nouveaux fichiers écrasent les anciens et celui où le cache est réchauffé, les requêtes tombent sur du code partiellement mis à jour. Trois stratégies permettent de fermer cette fenêtre — symlink atomic, blue-green et rolling deploy — et chacune repose sur des migrations Doctrine backward compatible. Cet article détaille la configuration Deployer, les règles de migration et la structure d'un workflow GitHub Actions commenté étape par étape.
- La fenêtre de vulnérabilité
- Les 3 stratégies sans downtime
- Migrations Doctrine sans interrompre le service
- Checklist de déploiement
- Workflow GitHub Actions complet
La fenêtre de vulnérabilité entre composer install et cache warmup
Quand vous mettez à jour une application Symfony en place, le serveur continue de recevoir du trafic pendant toute la durée de l'opération :
- git pull ou copie de fichiers : les sources sont partiellement à jour.
- composer install : les nouvelles classes arrivent dans vendor/.
- L'ancien cache var/cache/prod/ référence encore les classes et la configuration compilée de l'ancienne version.
- php bin/console cache:warmup : le cache est enfin cohérent avec le nouveau code.
Entre les étapes 2 et 4, chaque requête entrante tombe sur une combinaison non validée : nouveau code, ancien cache, conteneur DI obsolète. La durée de cette fenêtre dépend de la taille du projet. La solution n'est pas d'aller plus vite, c'est de préparer la nouvelle version hors ligne puis de basculer en une seule opération atomique.
Les 3 stratégies de déploiement sans downtime
1. Symlink atomic avec Deployer
Deployer prépare chaque déploiement dans un répertoire isolé — releases/YYYYMMDDHHIISS/ — puis exécute une opération atomique : ln -sfn releases/20241215120000 current. Le document root Nginx ou Apache pointe sur current/ en permanence. La bascule est imperceptible côté HTTP.
Points essentiels dans deploy.php :
- require 'recipe/symfony.php' : recette officielle, inclut les tâches cache:clear, cache:warmup et doctrine:migrations:migrate.
- set('keep_releases', 5) : cinq releases conservées pour un rollback immédiat via dep rollback prod.
- set('shared_dirs', ['var/log', 'var/sessions']) : logs et sessions persistent entre les déploiements.
- set('shared_files', ['.env.local']) : les variables de production ne sont jamais écrasées par un déploiement.
- after('deploy:failed', 'deploy:unlock') : libère automatiquement le verrou en cas d'échec partiel.
Le répertoire current/ est un lien symbolique vers la release active. Changer de version revient à déplacer ce lien en une seule opération.
2. Blue-green deployment
Deux environnements identiques coexistent : blue (production) et green (standby). Le déploiement suit trois temps :
- Déployer sur green et exécuter les smoke tests.
- Rediriger le load balancer (Nginx upstream, HAProxy backend) de blue vers green.
- Maintenir blue disponible pendant quelques minutes pour un rollback immédiat si une anomalie remonte après la bascule.
Cette stratégie permet de valider la nouvelle version sur l'infrastructure réelle avant tout contact avec le trafic de production. Elle exige un load balancer et une infrastructure doublée pendant la fenêtre de rollback. Pour un serveur unique sans LB, la stratégie symlink est plus adaptée.
3. Rolling deploy sur plusieurs nœuds
Sur une infrastructure multi-nœuds, chaque serveur est sorti du load balancer, mis à jour, puis réintégré, un par un. Le service reste disponible en permanence. La contrainte forte : la base de données doit être compatible avec les deux versions de l'application simultanément, ce qui impose des migrations Doctrine backward compatible strictes.
Migrations Doctrine sans interrompre le service
Le pattern expand-contract
Quelle que soit la stratégie de déploiement choisie, la base de données est partagée entre l'ancienne et la nouvelle version du code pendant la bascule. Une migration qui supprime ou renomme une colonne encore utilisée par l'ancienne version casse les requêtes actives.
Le pattern expand-contract décompose tout changement de schéma en plusieurs déploiements successifs :
- Expand : ajouter la nouvelle structure sans toucher l'ancienne (nouvelle colonne nullable, nouvelle table).
- Migrate data : alimenter la nouvelle structure via un job ou un backfill batch.
- Contract : supprimer l'ancienne structure, seulement après que le code de la version précédente ne tourne plus.
Exemple : ajout d'une colonne NOT NULL sur une table volumineuse
Un ALTER TABLE ... ADD COLUMN ... NOT NULL sur une table de plusieurs millions de lignes pose un verrou exclusif pendant toute la durée de la migration, bloquant les écritures. La séquence en trois déploiements évite ce verrou prolongé :
- Déploiement 1 : ajouter la colonne en NULL — l'ancienne version du code ne la connaît pas, elle continue de fonctionner sans changement.
- Entre les déploiements : backfill par batches — mettre à jour les lignes par tranches pour éviter un verrou table prolongé.
- Déploiement 2 : le nouveau code lit et écrit la colonne ; les nouvelles insertions la renseignent.
- Déploiement 3 : une fois toutes les lignes renseignées, passer la colonne en NOT NULL. La modification est rapide car aucune ligne ne contient NULL.
Pour les pratiques d'industrialisation globales sur Symfony, consultez l'article Maintenir une application Symfony : dépendances, version de PHP et mises à jour.
Checklist de déploiement Symfony
À exécuter dans l'ordre, avant de considérer un déploiement terminé :
- Sauvegarde vérifiée : snapshot de la base de données de moins de 24 h avant toute migration.
- composer install --no-dev --optimize-autoloader : vendors production, autoloader optimisé.
- doctrine:migrations:migrate --no-interaction --allow-no-migration : idempotent, ne bloque pas si aucune migration n'est en attente.
- cache:warmup --env=prod : cache applicatif et conteneur DI pré-compilés.
- Redémarrage des workers Messenger : messenger:stop-workers puis redémarrage supervisord — les workers gardent l'ancien code en mémoire jusqu'au redémarrage.
- Crons en pause pendant la migration, réactivés après le smoke test.
- Smoke test : la home et l'endpoint /health doivent répondre HTTP 200.
- Rollback documenté : dep rollback prod ou bascule LB exécutable sans intervention manuelle.
La page Mises à jour Sentinelo détaille comment cette checklist est automatisée pour les applications Symfony hébergées sur la plateforme.
Workflow GitHub Actions complet
Le workflow suit deux jobs séquentiels : test en prérequis, puis deploy conditionné à needs: test. Déclencheur : push sur la branche main.
Job test : checkout, PHP 8.3 via shivammathur/setup-php@v2, extensions intl et pdo_pgsql, composer install --prefer-dist --no-progress, exécution de la suite PHPUnit. Le job deploy ne démarre pas si un test échoue.
Job deploy — étapes dans l'ordre d'exécution :
- Configure SSH : écriture du secret DEPLOY_SSH_KEY dans ~/.ssh/id_ed25519 (chmod 600), puis ssh-keyscan du host de production pour éviter la vérification interactive.
- Install Deployer : téléchargement du phar depuis deployer.org/releases/current/deployer.phar.
- Deploy : ./deployer deploy crée le répertoire release, installe les vendors, chauffe le cache, bascule le symlink atomiquement.
- Migrate : connexion SSH sur le serveur, exécution de php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration. Cette étape s'exécute après le symlink, donc sur le code de la nouvelle release. Les migrations doivent respecter expand-contract : une migration ne peut pas casser la version précédente qui tourne encore en PHP-FPM.
- Restart workers : sudo systemctl restart symfony-messenger@monapp.service — le worker démarre avec le nouveau code chargé.
- Smoke test : boucle de six tentatives à cinq secondes d'intervalle, vérification HTTP 200 sur la home. Succès = déploiement stable.
- Rollback automatique sur if: failure() : ./deployer rollback rebascule le symlink vers la release précédente. Aucune migration inverse n'est nécessaire si elles respectent expand-contract.
Ce workflow couvre le cas le plus courant : un seul nœud sans load balancer. Pour un déploiement multi-nœuds, ajoutez une étape de retrait et de réintégration de chaque nœud dans le LB autour du déploiement unitaire.
Sentinelo détecte les nouvelles dépendances Composer disponibles pour vos applications Symfony et les signale sans les appliquer — c'est votre équipe qui décide du moment. Si vous souhaitez déléguer la surveillance des dépendances, des sauvegardes nocturnes et des certificats, l'offre maintenance Symfony couvre ces besoins avec un contrat de service. Retrouvez également les conseils sur la politique de mises à jour CMS en agence pour cadrer ces pratiques à l'échelle de votre organisation.
Questions fréquentes
Quelle est la différence entre un déploiement Deployer et un git pull en production ?
Deployer prépare la nouvelle version dans un répertoire isolé — vendors, cache warmup, configuration — avant toute bascule. Une fois tout prêt, il remplace le lien symbolique current/ en une seule opération atomique. Un git pull met à jour les fichiers en place, exposant une fenêtre pendant laquelle le code et le cache sont incohérents et les requêtes peuvent retourner des erreurs.
Peut-on déployer Symfony sans downtime sur un seul serveur, sans load balancer ?
Oui. La stratégie symlink atomic de Deployer fonctionne sur un seul nœud sans load balancer. Le document root pointe sur un lien symbolique, et la bascule vers la nouvelle release est instantanée au niveau du système de fichiers. Les requêtes en cours finissent leur cycle de vie sur l'ancien code, les nouvelles requêtes arrivent sur le nouveau : il n'y a pas de coupure visible.
Comment s'assurer qu'une migration Doctrine ne cassera pas la production ?
Appliquez le pattern expand-contract : n'ajoutez, ne supprimez ni ne renommez jamais une colonne en une seule migration lorsque l'ancienne version du code tourne encore. Décomposez en plusieurs déploiements successifs — ajout de la nouvelle structure, remplissage, puis suppression de l'ancienne. Testez la migration sur une copie de production avec des volumes représentatifs avant de l'appliquer.
Que faire si un déploiement Symfony échoue après la bascule du symlink ?
Deployer conserve les releases précédentes par défaut. Un rollback se fait avec la commande dep rollback prod : le symlink rebascule immédiatement vers la version précédente, sans toucher la base de données. C'est précisément pour cela que les migrations doivent être backward compatible : l'ancienne version du code doit fonctionner avec le nouveau schéma pendant toute la durée du rollback.
Comment GitHub Actions déclenche-t-il un rollback automatique si le smoke test échoue ?
Dans le workflow, l'étape de rollback est conditionnée à if: failure() et appelle ./deployer rollback. Si la boucle de smoke test détecte un code HTTP autre que 200 après six tentatives, le job passe en état d'échec et cette condition déclenche le rollback automatiquement. Deployer rebascule le symlink vers la release précédente en quelques secondes, sans intervention manuelle.
Pour aller plus loin
Articles liés
Un parc à superviser ? Voyez ce que Sentinelo détecte chez vous.
Scanner mon site