Symfony 7 : dépannage erreurs 500, var/cache, Composer et variables d’environnement

Playbook pratique pour identifier et corriger 90% des 500 sous Symfony 7 : triage Nginx/PHP-FPM/Symfony, permissions var/cache, Composer et env.

Interface numérique avec des icônes de programmation et variables d'environnement Symfony.

Table des matières :

  1. 500 en Symfony 7 : isoler la couche fautive (HTTP, PHP-FPM, Symfony)
  2. var/cache en Symfony 7 : ce que tu casses quand tu “clear” en prod
  3. Composer : vendor/ cassé, autoload incohérent, scripts Flex qui échouent
  4. Variables d’environnement : APP_ENV, APP_DEBUG, DSN, secrets… et l’ordre de résolution réel
  5. Un playbook de dépannage (répétable) pour éliminer 90% des 500 Symfony 7

Un 500 Internal Server Error sur une app Symfony 7 n’est pas une “erreur Symfony”. C’est un symptôme : quelque part entre le reverse-proxy, PHP-FPM et le Kernel, une exception n’a pas été rendue correctement, ou un composant critique (cache, autoload, env) n’est plus cohérent.

500 en Symfony 7 : isoler la couche fautive (HTTP, PHP-FPM, Symfony)

Le point de départ, c’est la définition protocolaire. L’IETF décrit le statut 500 ainsi :

« The 500 (Internal Server Error) status code indicates that the server encountered an unexpected condition that prevented it from fulfilling the request. » — RFC 9110, section 15.6.1 (RFC Editor, 2022)
https://www.rfc-editor.org/rfc/rfc9110.html#section-15.6.1

Concrètement, ce code ne te dit rien sur la cause : ça peut être un segfault PHP, un timeout FPM, une exception Doctrine, une route cassée, un cache illisible, un vendor/ incomplet… ou même un upstream (API) qui renvoie autre chose, masqué par un proxy.

Avant de “regarder Symfony”, tranche rapidement où se casse la chaîne :

  • Front (CDN/WAF/LB) → Nginx/Apache : erreurs réseau, TLS, timeouts, upstream indisponible (souvent plutôt 502/504, mais certaines configs renvoient un 500 générique).
  • Nginx/Apache → PHP-FPM : sockets, saturation pm.max_children, “connection refused”, “upstream timed out”.
  • PHP runtime : fatal error, memory limit, extension manquante.
  • Symfony (Kernel / Container / Twig / Doctrine) : exception applicative, container cassé, config invalide.

Un aide-mémoire pratique (à corréler avec tes logs) :

Symptôme côté navigateur / proxy Indice dans les logs Couche la plus probable Premier réflexe
500 immédiat après déploiement Class not found, Failed opening required vendor/autoload.php Composer / autoload vérifier vendor/ + composer install
500 aléatoire sous charge upstream timed out / server reached pm.max_children FPM saturation vérifier FPM + slowlog + ressources
500 sur une route précise stacktrace dans var/log/prod.log Symfony / code isoler la requête + exception
page blanche (sans 500 visible) fatal PHP sans affichage PHP runtime activer logs PHP + vérifier error_log

En prod, la première erreur est de “regarder Symfony” avant de regarder le serveur. Tant que tu n’as pas tranché entre Nginx/Apache, PHP-FPM et Symfony, tu vas perdre du temps. Exemple de triage rapide côté Nginx :

# Nginx
sudo tail -n 200 /var/log/nginx/error.log

# Apache (si applicable)
sudo tail -n 200 /var/log/apache2/error.log

# PHP-FPM (Debian/Ubuntu)
sudo tail -n 200 /var/log/php8.3-fpm.log
# ou via systemd
sudo journalctl -u php8.3-fpm --since "10 min ago" -n 200
  • Si tu vois un upstream prematurely closed connection ou connect() failed (111: Connection refused) : tu es plutôt dans le monde FPM / sockets (voire saturation de workers).
  • Si tu vois une fatale PHP (Allowed memory size exhausted, Class not found) : c’est du runtime PHP ou de l’autoload.
  • Si tu vois une stack Symfony dans var/log/prod.log : c’est du Kernel / DI / Twig / Doctrine.

Pour gagner du temps, collecte systématiquement (même sur un petit serveur) : URL, timestamp, IP, User-Agent, et idéalement un identifiant de requête (voir plus bas). Sans ça, tu “répares” à l’aveugle.

Côté Symfony 7, vérifie ensuite les logs applicatifs. Par défaut (Monolog standard), tu as var/log/prod.log et var/log/dev.log (selon config). En prod, éviter le réflexe APP_DEBUG=1 “sur le serveur live” : en plus du risque sécurité, tu vas créer des conditions non représentatives (cache, compilation du container, etc.). Active plutôt le debug sur staging (ou via un environnement cloné) et utilise un vrai débogueur si besoin ; tu peux t’appuyer sur ce guide interne : Xdebug VS Code : configurer le débogage PHP pas à pas.

Un point souvent sous-estimé : la différence entre les variables visibles en SSH et celles visibles par PHP-FPM. Une app qui “marche en CLI” mais fait 500 via HTTP est très souvent un problème de SAPI (FPM) : env non injectées, permissions, chemin différent, etc.

Enfin, garde en tête les erreurs “fausses 500” en frontal (CDN/WAF/proxy) : un 502/504 peut être masqué, ou un 500 peut venir d’un upstream différent (API). Si tu es sur une stack e-commerce ou un hébergement managé (fréquent sur des parcs multi-sites en France), documente clairement le chemin de la requête (CDN → LB → Nginx → FPM → app) et où tu peux réellement accéder aux logs. Pour structurer cette analyse côté infra/perf, la démarche détaillée dans Symfony 7 : déploiement PHP-FPM Nginx, cache et opcache est une bonne grille de lecture.

var/cache en Symfony 7 : ce que tu casses quand tu “clear” en prod

Dans Symfony 7, var/cache/<env>/ n’est pas un cache “optionnel”. C’est un artefact d’exécution : container compilé, routes, métadonnées, templates Twig compilés, etc. Si le contenu est incohérent avec ton code (ou illisible), tu peux obtenir un 500 avant même de servir la moindre page.

Les symptômes typiques côté logs :

  • Failed to write cache file (souvent disque plein, permissions, inode, FS read-only)
  • Permission denied (owner/ACL/umask incohérente)
  • Class ... not found while compiling the container (déploiement incomplet, autoload cassé)
  • Cannot redeclare class (release non atomique, mélange de versions en cache/opcache)

La commande bin/console cache:clear --env=prod fait plus que “vider un dossier”. Elle reconstruit le container, et elle peut déclencher des warmers liés à Doctrine/Twig. En prod, une stratégie fiable est : build et warmup hors trafic, puis bascule atomique (release par symlink). Exemple de séquence (dans le répertoire release, pas dans le répertoire live) :

php bin/console cache:clear --no-warmup --env=prod
php bin/console cache:warmup --env=prod

Deux points concrets à vérifier quand ça “semble” un problème de cache :

1) Le cache est-il writable par le user qui exécute PHP-FPM ?
Le piège classique, c’est d’exécuter cache:clear avec l’utilisateur SSH (deploy) alors que PHP-FPM tourne en www-data (ou inversement). Résultat : cache régénéré avec de mauvais owners/permissions, puis 500 au premier hit.

2) Ton déploiement est-il atomique ?
Si tu modifies le code en place (sans release directory), tu peux avoir simultanément : un vendor/ partiellement remplacé, un container compilé sur l’ancien code, et de l’opcache encore “chaud”. Le 500 devient intermittent et difficile à reproduire.

Pour auditer rapidement permissions + chemins (sans supposer), ces commandes donnent une vue utile :

# Quel user fait tourner FPM ?
ps aux | grep php-fpm | head

# Vérifier propriétaires et droits (exemples)
ls -la var/ var/cache var/cache/prod | sed -n '1,120p'

# Visualiser chaque segment du chemin (pratique en cas de "Permission denied")
namei -l var/cache/prod

Sur Linux, deux approches robustes (et évite chmod -R 777 var) :

1) ACL (setfacl) sur var/ pour autoriser deploy et www-data à écrire ; 2) exécuter les commandes console avec le même user que FPM (via sudo -u www-data) si ton modèle d’accès le permet.

Exemple ACL (à adapter) :

# Donne à deploy et www-data l'accès en lecture/écriture/exécution sur var/
sudo setfacl -R -m u:deploy:rwx -m u:www-data:rwx var
# Définit les ACL par défaut pour les nouveaux fichiers/dossiers
sudo setfacl -R -d -m u:deploy:rwx -m u:www-data:rwx var

Sur Docker/Kubernetes, c’est encore plus fréquent : volume monté en root, www-data ne peut pas écrire, puis 500 immédiat. Dans ce cas, le correctif n’est pas “vider le cache” mais corriger l’UID/GID (securityContext), ou définir un fsGroup côté K8s.

Enfin, nuance utile : tout n’est pas “var/cache”. Si ton 500 est lié à un cache applicatif (PSR-6/PSR-16), tu peux parfois vider un pool sans toucher au container (et donc sans reconstruire routes/services). Quand c’est applicable, c’est moins risqué qu’un cache:clear en production.

Composer : vendor/ cassé, autoload incohérent, scripts Flex qui échouent

Composer est un point de rupture majeur parce que Symfony 7 dépend fortement d’un graphe de packages et de scripts. Le rappel utile (et officiel) est simple :

« Composer is a dependency manager for PHP. » — getcomposer.org
https://getcomposer.org/

Donc si ton vendor/ ne correspond plus au composer.lock, tu peux obtenir des 500 “bizarres” (classes manquantes, interfaces incompatibles) qui ressemblent à des erreurs d’app, alors que c’est une erreur d’assemblage.

En déploiement, la règle est : composer install en prod, pas composer update. install respecte composer.lock, update peut te sortir un nouveau set de versions, et donc casser un comportement runtime (y compris via des dépendances transverses : PSR-*, Symfony Contracts, Doctrine, Twig).

Exemple de commande de build “clean” en release :

composer install \
  --no-dev \
  --prefer-dist \
  --no-interaction \
  --optimize-autoloader

Si tu veux pousser plus loin l’optimisation, tu peux ajouter --classmap-authoritative, mais uniquement si tu maîtrises le fait que l’autoload ne “scannera” plus dynamiquement. Ça peut casser des plugins ou du code qui s’appuie sur des classes ajoutées “hors Composer” sans dump-autoload.

Deux diagnostics rapides à exécuter quand un 500 apparaît après un déploiement :

# Vérifie que la plateforme PHP/ext correspond à composer.lock
composer check-platform-reqs

# Vérifie la cohérence du fichier composer.json
composer validate

Ajoute un troisième réflexe quand l’erreur ressemble à un autoload incohérent (classes introuvables, traits absents, etc.) :

composer dump-autoload -o

Mini-scenario (très réaliste) : tu fais un déploiement via rsync/FTP, et vendor/ est partiellement uploadé quand le trafic arrive. Résultat : une partie des requêtes passe (classes déjà en opcache), et d’autres font 500 (classes pas encore présentes). C’est une des raisons pour lesquelles on évite de déployer vendor/ “en place” : build en amont + release atomique + rollback immédiat.

Les cas réels les plus fréquents sur Symfony 7 :

  • mismatch version PHP (ex. build en PHP 8.3, runtime en PHP 8.2) : les dépendances installées peuvent être différentes ;
  • extensions manquantes en prod (ext-intl, ext-sodium, ext-redis) : parfois, tu ne le vois qu’à l’exécution (ex. un adapter Redis instancié au boot du container) ;
  • scripts post-install (Flex) qui échouent et laissent un état incomplet.
  • Astuce de dépannage : si tu suspectes un script Composer (recipes Flex, cache warmers, etc.), relance un install en mode verbeux et/ou isole les scripts :

    composer install -vvv
    
    # Pour diagnostiquer un script qui casse :
    composer install --no-scripts -vvv
    

    Si --no-scripts “fait disparaître” le problème, tu sais que la casse est dans les hooks (post-install/post-update). Ça ne corrige pas, mais ça localise.

    Pour comprendre l’impact réel de Flex (recettes, scripts, conventions) dans l’outillage et le workflow, tu peux compléter avec Symfony : composants réutilisables, Flex et productivité développeur.

    Variables d’environnement : APP_ENV, APP_DEBUG, DSN, secrets… et l’ordre de résolution réel

    Le cœur du problème “variables d’environnement” n’est pas Symfony : c’est le déploiement. Le principe le plus clair reste celui du Twelve-Factor App :

    « Store config in the environment. » — The Twelve-Factor App (2011)
    https://12factor.net/config

    En Symfony 7, si tu comptes sur des .env en prod, tu prends le risque d’un 500 au moindre écart entre le fichier, les permissions, ou l’outil de démarrage. Le bon modèle : variables injectées au runtime (systemd, Docker env, Kubernetes Secret), ou build d’un artefact figé quand c’est assumé.

    Les pièges “classiques” qui déclenchent un 500

    Typiquement, un 500 arrive quand DATABASE_URL est absent/mal formé, quand APP_SECRET est vide, ou quand tu changes APP_ENV (ex. prod → dev) sans régénérer le cache. Exemples concrets :

    • DATABASE_URL="mysql://user:pass@db:3306/app?serverVersion=8.0" : si serverVersion est faux, Doctrine peut générer des SQL invalides (parfois uniquement sur une fonctionnalité ou une migration).
    • REDIS_URL non défini alors qu’un cache adapter Redis est configuré : crash au boot du container, donc 500 dès la première requête.
    • MAILER_DSN invalide : certaines apps instancient le transport au démarrage et plantent “avant” même de servir une page.
    • Et surtout : l’environnement vu par PHP-FPM n’est pas celui de ta session SSH. Par défaut, beaucoup de configs PHP-FPM nettoient l’environnement (clear_env) et ne passent pas tes variables. Résultat : bin/console marche (car tu exportes les variables dans ton shell), mais le site fait 500 en HTTP.

      Référence officielle PHP-FPM (utile pour comprendre clear_env et env[]) : https://www.php.net/manual/en/install.fpm.configuration.php

      ; /etc/php/8.x/fpm/pool.d/www.conf
      clear_env = no
      env[APP_ENV] = prod
      env[APP_DEBUG] = 0
      env[DATABASE_URL] = mysql://...
      

      À noter : selon ton contexte, tu préféreras injecter via systemd (Environment= / EnvironmentFile=) ou via Docker/Kubernetes. L’idée est la même : garantir la présence des variables au runtime HTTP.

      .env en prod : si tu n’as pas le choix, fige-le

      Pour une app Symfony 7 “classique” avec Flex, le chargement des .env sert surtout en dev. En prod, la doc Symfony recommande de configurer l’environnement et de déployer proprement : https://symfony.com/doc/current/deployment.html

      Si tu dois malgré tout t’appuyer sur des .env en prod (contrainte legacy, hébergeur limité), fige la config et évite le parsing à chaque requête. Le mécanisme standard est composer dump-env prod, qui génère un .env.local.php (table PHP précompilée). C’est plus rapide et réduit les surprises liées à la lecture de fichiers, mais ça reste une config build-time : au moindre changement, tu rebuild (et tu warm le cache).

      Le piège “Secrets” : clé absente = crash tôt

      Le dernier piège : les Symfony Secrets. Si tu utilises bin/console secrets:* et que le serveur n’a pas la clé adéquate (fichier de clé privée ou variable de déchiffrement, selon ton mode de déploiement), tu peux provoquer un 500 dès la compilation du container… ou un comportement “ça marche en CLI mais pas via FPM” si les env ne sont pas identiques.

      Sur des stacks qui brassent plusieurs apps (ex. back-office e-commerce + microservices), documente exactement où vivent les secrets (Vault, Secret Manager, fichiers chiffrés) et qui a la responsabilité du cycle de vie : génération, rotation, et procédure de rollback.

      Un playbook de dépannage (répétable) pour éliminer 90% des 500 Symfony 7

      Le playbook efficace évite le mode “bidouille”. Étape 1 : figer l’état. Note le hash de release, la version PHP, le timestamp du premier 500, et ne relance pas dix fois cache:clear “pour voir” (tu risques de masquer la cause en changeant l’état du système).

      Étape 2 : corréler logs. Si possible, ajoute un request_id dans le log format Nginx/Apache et propage-le (header) vers PHP/Symfony : sans identifiant, tu perds le lien entre error.log, php-fpm.log et prod.log dès qu’il y a de la concurrence.

      Checklist de triage (orientée action, 10–15 minutes) :

      1) Serveur web : erreurs upstream, timeout, taille de réponse, permissions de fichier statique. 2) PHP-FPM : pool up ? saturation ? crash ?

      • indicateur typique : erreurs server reached pm.max_children ou timeouts. 3) Logs Symfony : var/log/prod.log autour du timestamp exact. 4) Composer/autoload : composer check-platform-reqs + vérifier intégrité de vendor/. 5) Cache/permissions : var/cache/prod writable par FPM ? (owners, ACL, FS en read-only, disque plein). 6) Env runtime : variables présentes côté FPM (pas juste en SSH) : DATABASE_URL, APP_SECRET, DSN. 7) Rollback si tu n’isoles pas vite : un rollback atomique est souvent la meilleure “réparation” immédiate, le temps d’analyser.
      • Ensuite, reproduis hors prod. Le diagnostic d’un 500 Symfony 7 devient beaucoup plus simple quand tu peux exécuter la requête en environnement cloné avec APP_DEBUG=1 et un profiler. Dans une organisation e-commerce/agence, c’est exactement la discipline à appliquer : staging fidèle, déploiements atomiques, rollback. Le guide Staging PrestaShop : créer un environnement Docker fidèle à la production donne un bon modèle de rigueur (même si les détails sont PrestaShop).

        Enfin, investis dans l’observabilité minimale (sans sur-industrialiser) :

        • métriques système (CPU, RAM, IO, espace disque) : un 500 peut être une conséquence d’OOM, de disque plein, ou d’une latence disque sur var/cache ;
        • suivi de la concurrence PHP-FPM (queue, pm.max_children, slowlog) ;
        • alerting sur augmentation de taux d’erreurs (4xx/5xx) et sur temps de réponse.
        • Si tu n’as rien en place, Netdata permet de monter rapidement un socle de supervision exploitable, y compris dans des environnements conteneurisés ; voir Netdata collecteurs : supervision temps réel Kubernetes, Docker et AWS.

          Dans la vraie vie, ce playbook te donne un chemin court :

          1) logs Nginx/Apache → 2) logs PHP-FPM → 3) var/log/prod.log → 4) vérif vendor/ (composer check-platform-reqs) → 5) permissions var/cache → 6) env vars manquantes → 7) rollback si tu ne peux pas isoler sous 15 minutes.

          Et si le symptôme est une “page blanche” plutôt qu’un 500 explicite (souvent display_errors=0 + fatale), la méthode de diagnostic décrite dans PrestaShop page blanche : diagnostiquer l’erreur fatale PHP rapidement reste directement applicable côté PHP/SAPI : l’objectif est d’obtenir la fatale exacte dans un log, pas d’activer l’affichage en production.

          Ce n’est pas glamour, mais c’est ce qui évite les 500 qui reviennent : des builds reproductibles (Composer + lock), un cache géré comme un artefact, et une configuration par environnement maîtrisée (au sens strict). Pour le rappel des exigences de plateforme (PHP 8.2+ en Symfony 7, cycles de support), garde sous la main : https://www.expertise-prestashop.fr/2026/08/20/symfony-7-exigences-php-8-2-lts-et-usage-en-entreprise/

          Ce n’est pas glamour, mais c’est ce qui évite les 500 qui reviennent : des builds reproductibles (Composer + lock), un cache géré comme un artefact, et une configuration par environnement maîtrisée (au sens strict). Pour le rappel des exigences de plateforme (PHP 8.2+ en Symfony 7, cycles de support), garde sous la main : https://www.expertise-prestashop.fr/2026/08/20/symfony-7-exigences-php-8-2-lts-et-usage-en-entreprise/

          Ce n’est pas glamour, mais c’est ce qui évite les 500 qui reviennent : des builds reproductibles (Composer + lock), un cache géré comme un artefact, et une configuration par environnement maîtrisée (au sens strict). Pour le rappel des exigences de plateforme (PHP 8.2+ en Symfony 7, cycles de support), garde sous la main : https://www.expertise-prestashop.fr/2026/08/20/symfony-7-exigences-php-8-2-lts-et-usage-en-entreprise/

          Ce n’est pas glamour, mais c’est ce qui évite les 500 qui reviennent : des builds reproductibles (Composer + lock), un cache géré comme un artefact, et une configuration par environnement maîtrisée (au sens strict). Pour le rappel des exigences de plateforme (PHP 8.2+ en Symfony 7, cycles de support), garde sous la main : https://www.expertise-prestashop.fr/2026/08/20/symfony-7-exigences-php-8-2-lts-et-usage-en-entreprise/


          À lire aussi