Skip to Content

Chapitre 10.2 — Symfony en prod

⏱️ TL;DR — Symfony tourne sur le PHP-FPM du chapitre précédent, mais un déploiement prod ne se résume pas à copier les fichiers. Il faut basculer en environnement prod (APP_ENV=prod, APP_DEBUG=0), installer les dépendances sans le dev (composer install --no-dev --optimize-autoloader), réchauffer le cache compilé, donner à www-data les droits d’écriture sur var/ (cache + logs) et rien d’autre, pointer le root Nginx sur public/ (jamais la racine du projet), compiler les assets, activer OPcache (voire le preload), et jouer les migrations Doctrine. Rate un seul de ces points et tu récoltes une erreur 500, une fuite de config, ou un site qui rame. Ce chapitre est la checklist de mise en prod Symfony.

🎯 Objectifs

  • Configurer l’environnement prod proprement (APP_ENV, APP_DEBUG, .env.local, dump-env).
  • Installer les dépendances optimisées et réchauffer le cache compilé.
  • Régler les permissions de var/ sans tout ouvrir en grand.
  • Pointer Nginx sur public/ et écrire le server block recommandé.
  • Gérer assets, OPcache/preload et migrations au déploiement.

Symfony en prod : un état d’esprit, pas une copie

En local, tu lances symfony serve ou php bin/console et tout « marche ». En prod, Symfony fonctionne différemment : il précompile un maximum de choses (le conteneur de services, le routing, le cache) pour ne rien recalculer à chaque requête. C’est ça, le mode prod. Le déploiement consiste à préparer cet état compilé et à donner à PHP-FPM de quoi le lire et écrire ses logs.

1. L’environnement : APP_ENV=prod, APP_DEBUG=0

Symfony lit son environnement dans des variables. Les deux décisives :

  • APP_ENV=prod : active l’environnement de production (cache compilé, pas de barre de debug, routing optimisé).
  • APP_DEBUG=0 : coupe le mode debug. Le laisser à 1 en prod est une faille : les pages d’erreur exposent le code, les chemins, la config, parfois des secrets.

En dev, ces valeurs vivent dans .env (versionné). En prod, on ne modifie pas .env versionné : on crée un .env.local (non versionné) qui surcharge, ou on injecte les variables via l’environnement système / le service systemd.

# .env.local (NON versionné, sur le serveur uniquement) APP_ENV=prod APP_DEBUG=0 APP_SECRET=un_secret_long_et_aleatoire_genere_pour_la_prod DATABASE_URL="postgresql://formacampus:motdepasse@127.0.0.1:5432/formacampus?serverVersion=16&charset=utf8"

Pour la perf, on compile ces variables plutôt que de parser les fichiers .env à chaque requête :

# Génère un .env.local.php optimisé, lu directement par Symfony en prod composer dump-env prod

⚠️ Piège — Laisser APP_DEBUG=1 (ou déployer avec la barre de debug active). Une erreur affiche alors une stack trace Symfony complète au public : chemins serveur, requêtes SQL, variables d’environnement. C’est un cadeau pour un attaquant. En prod : APP_DEBUG=0, toujours. Vérifie-le après chaque déploiement en provoquant volontairement un 404 : tu dois voir une page d’erreur sobre, pas la page de debug colorée.

2. Les dépendances : composer install --no-dev

En prod, on n’installe pas les dépendances de développement (PHPUnit, debug bar, faker…) et on optimise l’autoloader.

# --no-dev : pas les paquets require-dev ; --optimize-autoloader : classmap complète composer install --no-dev --optimize-autoloader

--optimize-autoloader génère une classmap complète : Composer sait dès le départ où trouver chaque classe, sans scruter le disque à chaque new. Combiné à OPcache, c’est un vrai gain.

💡 Réflexe — Sur le serveur, tu peux régler APP_ENV=prod avant composer install : Symfony Flex n’exécutera alors que les scripts d’auto-scripts pertinents pour la prod (dont le warmup du cache). Beaucoup d’équipes préfèrent même builder ailleurs (en CI) et n’envoyer sur le VPS que le résultat prêt — on y vient en Partie 13.

3. Le cache : vider puis réchauffer

Symfony compile un cache (conteneur de services, routes, annotations…). En prod, on le vide puis on le réchauffe pour que la première requête réelle ne paie pas le coût de compilation.

# Vide le cache de l'environnement prod php bin/console cache:clear --env=prod --no-debug # Le pré-génère (warmup) pour que la 1re vraie requête soit rapide php bin/console cache:warmup --env=prod

Note le --env=prod : sans lui, la console vise l’environnement par défaut. On veut agir sur prod.

4. Les permissions de var/ : le nerf de la guerre

C’est la source d’erreurs 500 en déploiement Symfony. Symfony écrit dans var/ : var/cache (cache compilé) et var/log (les logs). Or c’est PHP-FPM, tournant en www-data, qui exécute l’appli. Donc www-data doit pouvoir écrire dans var/ — mais nulle part ailleurs.

# www-data doit posséder (ou pouvoir écrire) var/ sudo chown -R deploy:www-data /var/www/formacampus-api # Le code : lecture seule pour le groupe ; var/ : écriture pour le groupe sudo find /var/www/formacampus-api -type d -exec chmod 750 {} \; sudo find /var/www/formacampus-api -type f -exec chmod 640 {} \; # var/ (et son contenu) inscriptible par le groupe www-data sudo chmod -R 770 /var/www/formacampus-api/var

⚠️ Piège — Deux extrêmes à éviter. (1) Trop fermé : www-data ne peut pas écrire var/ → erreur 500 opaque (Unable to write in the cache directory). (2) Trop ouvert : chmod -R 777 sur tout le projet « pour que ça marche » — c’est une faille béante, n’importe quel process peut réécrire ton code. La règle : le code en lecture seule pour www-data, seul var/ en écriture. Jamais 777, jamais l’écriture sur le code source.

🔒 Sécurité — Idéalement, le user qui déploie (deploy) possède les fichiers, et www-data n’a que ce qu’il lui faut (lecture du code, écriture de var/). Ce cloisonnement fait qu’une faille dans l’appli PHP ne permet pas de réécrire le code déployé. C’est le même principe d’isolation que les pools FPM du chapitre 10.1.

5. Le document root : public/, jamais la racine

Point capital. Une appli Symfony expose uniquement son dossier public/ (qui contient index.php, le front controller, et les assets buildés). Tout le reste — src/, config/, .env, vendor/, var/ — doit rester hors de portée du web.

⚠️ Piège — Pointer le root Nginx sur la racine du projet au lieu de public/. Conséquence : .env (avec ta DATABASE_URL et tes secrets), config/, le code source deviennent téléchargeables par n’importe qui via une URL. C’est l’une des fuites les plus classiques et les plus graves. Le root pointe toujours public/, point final.

6. Le server block Nginx recommandé

Symfony fournit un modèle officiel. En voici l’essence, adapté à FPM (chapitre 10.1). La subtilité : on n’autorise qu’un seul index.php à s’exécuter (le front controller), pour empêcher l’exécution d’autres .php planqués.

server { listen 80; server_name api.formacampus.fr; # LE point clé : la racine web est public/, pas le projet root /var/www/formacampus-api/public; location / { # Tout ce qui n'est pas un vrai fichier passe par le front controller try_files $uri /index.php$is_args$args; } # On n'autorise que index.php à passer en FastCGI (front controller unique) location ~ ^/index\.php(/|$) { fastcgi_pass unix:/run/php/php8.3-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $document_root; # Empeche l'URI de "fuir" au-dela du front controller internal; } # Tout autre .php est refusé (defense en profondeur) location ~ \.php$ { return 404; } # Logs dédiés à ce vhost error_log /var/log/nginx/formacampus-api_error.log; access_log /var/log/nginx/formacampus-api_access.log; }

try_files $uri /index.php$is_args$args sert le fichier s’il existe (image, CSS buildé…), sinon route tout vers index.php — le routeur Symfony prend alors le relais. Le second location ~ \.php$ { return 404; } interdit d’exécuter n’importe quel autre .php : seul le front controller vit.

7. Les assets

Selon l’appli, Symfony gère les assets via AssetMapper (l’approche moderne, sans build Node) ou Webpack Encore (build Node classique). Dans les deux cas, on prépare les assets au déploiement.

# AssetMapper : compile et versionne les assets (hash dans le nom) php bin/console asset-map:compile --env=prod # OU, si le projet utilise Webpack Encore : npm ci && npm run build

Les fichiers produits atterrissent dans public/ (souvent public/assets/ ou public/build/) et sont servis directement par Nginx en statique, sans passer par PHP — exactement l’idée du chapitre 10.4.

8. OPcache et preload

OPcache (chapitre 10.1) est indispensable pour Symfony. En plus, Symfony peut générer un fichier de preload : PHP charge en mémoire, une fois pour toutes au démarrage de FPM, les classes du framework les plus utilisées. Gain de latence appréciable.

; php.ini de FPM — active le preload Symfony (le chemin est généré au warmup) opcache.preload=/var/www/formacampus-api/var/cache/prod/App_KernelProdContainer.preload.php opcache.preload_user=www-data

💡 Réflexe — Le preload est figé au démarrage de FPM. Après chaque déploiement, il faut donc recharger FPM (systemctl reload php8.3-fpm) pour que le nouveau code (et le nouveau fichier de preload) soit pris en compte. Mets ce reload dans ton script de déploiement, juste après le warmup du cache.

9. Les migrations Doctrine

Le schéma de base évolue avec le code. Au déploiement, après avoir mis le nouveau code en place et avant d’ouvrir le trafic, on applique les migrations Doctrine en attente.

# Applique les migrations non encore jouées, sans interaction php bin/console doctrine:migrations:migrate --no-interaction --env=prod

⚠️ Piège — Ne jamais utiliser doctrine:schema:update --force en prod pour « synchroniser » la base. Il fait ce qu’il veut, peut supprimer des colonnes et des données. En prod, on passe exclusivement par des migrations versionnées, relues, réversibles. schema:update est un outil de dev, pas de prod.

🧭 Sur FormaCampus — L’API Symfony de FormaCampus est le cœur de la migration vers le VPS. Elle vit dans /var/www/formacampus-api, servie sur api.formacampus.fr par le server block ci-dessus (root sur public/), exécutée par le pool PHP-FPM du chapitre 10.1, et parle à Postgres en local via DATABASE_URL dans .env.local (jamais versionné). Le déploiement suit la checklist dans l’ordre : composer install --no-dev, dump-env prod, cache:clear/warmup, permissions de var/ calées pour www-data, doctrine:migrations:migrate, puis reload php8.3-fpm pour rafraîchir OPcache et le preload. Le jour où l’API renvoie un 500 juste après un déploiement, le premier réflexe de l’équipe est de lire var/log/prod.log et de vérifier les droits de var/ — la cause numéro un.

📚 La doc — Symfony fournit un guide de déploiement officiel (« Deploying Symfony to Production ») et un modèle de configuration Nginx à jour dans « Setting up or Fixing File Permissions ». Le cours Symfony du hub couvre le framework côté développement (conteneur, routing, Doctrine, sécurité) — ce chapitre-ci ne traite que sa mise en production sur le VPS.

✏️ Exercices

Exercice 1 — La checklist de déploiement. Écris, dans l’ordre, la séquence de commandes qui déploie une nouvelle version de l’API Symfony de FormaCampus une fois le code à jour sur le serveur (env prod, dépendances, cache, migrations, rechargement FPM).

✅ Solution

cd /var/www/formacampus-api composer install --no-dev --optimize-autoloader composer dump-env prod php bin/console cache:clear --env=prod --no-debug php bin/console cache:warmup --env=prod php bin/console doctrine:migrations:migrate --no-interaction --env=prod sudo systemctl reload php8.3-fpm

On peut ajouter la compilation des assets (asset-map:compile ou npm run build) avant le warmup, et un ajustement des permissions de var/ si nécessaire. L’ordre compte : dépendances → env → cache → migrations → reload FPM (pour rafraîchir OPcache/preload).

Exercice 2 — Le .env téléchargeable. Un collègue déploie une API Symfony, met le root Nginx sur /var/www/api (la racine du projet). Tout marche… mais tu tapes https://api.exemple.fr/.env et le fichier se télécharge, secrets compris. Explique la faute et la correction.

✅ Solution

La faute : root pointe la racine du projet au lieu de public/. Du coup, tout le contenu du projet (.env, config/, src/, var/…) est exposé au web et téléchargeable. Correction : pointer root sur /var/www/api/public. Symfony est conçu pour que seul public/ soit exposé ; tout le reste doit rester hors de portée HTTP. Après correction : régénérer le APP_SECRET et les mots de passe qui ont pu fuiter, par prudence.

Exercice 3 — Erreur 500 après déploiement. Le site tournait, tu déploies, et toutes les pages renvoient un 500 générique. var/log/prod.log mentionne « Unable to write in the cache directory ». Diagnostic et remède ?

✅ Solution

www-data (l’utilisateur de PHP-FPM) n’a pas le droit d’écrire dans var/cache. C’est le piège des permissions. Remède : donner au groupe www-data l’écriture sur var/ (par ex. sudo chown -R deploy:www-data var && sudo chmod -R 770 var), sans ouvrir le reste du projet en écriture (surtout pas chmod 777 partout). Puis revider/réchauffer le cache. On garde le code en lecture seule pour www-data, seul var/ inscriptible.

🧠 Quiz de révision

1. Quelles deux variables d’environnement définissent le mode production de Symfony, et pourquoi la seconde est-elle une question de sécurité ?

APP_ENV=prod (active le cache compilé et l’optimisation) et APP_DEBUG=0. Laisser APP_DEBUG=1 en prod affiche des stack traces complètes au public (chemins, SQL, config, secrets) : c’est une faille. En prod, APP_DEBUG=0 toujours.

2. Pourquoi le root Nginx doit-il pointer sur public/ et pas sur la racine du projet ?

Parce que seul public/ est fait pour être exposé (il contient le front controller index.php et les assets). Pointer sur la racine rend .env, config/, src/, vendor/ téléchargeables — fuite de secrets et de code. Le root pointe toujours public/.

3. Quel dossier Symfony doit être inscriptible par www-data, et lequel ne doit surtout pas l’être ?

var/ (cache et logs) doit être inscriptible par www-data. Le code source (src/, config/, vendor/) doit rester en lecture seule pour lui. On ne fait jamais chmod 777 sur tout le projet : seul var/ s’ouvre en écriture.

4. Que fait composer install --no-dev --optimize-autoloader et quand le lance-t-on ?

Il installe les dépendances sans les paquets de développement et génère une classmap optimisée (l’autoloader trouve chaque classe sans scruter le disque). On le lance au déploiement en prod, pour une appli plus légère et plus rapide.

5. Pourquoi utilise-t-on doctrine:migrations:migrate et pas doctrine:schema:update --force en prod ?

Parce que les migrations sont versionnées, relues et contrôlées : on sait exactement ce qui change. schema:update --force synchronise « à l’aveugle » et peut supprimer des colonnes et des données. C’est un outil de dev, à proscrire en production.


Chapitre suivant : WordPress en prod — installer et durcir un WordPress sur une pile LEMP, avec base dédiée et permissions saines.

Last updated on