Table des matières :
- Prérequis réalistes (versions, OS, runtime) et points de friction
- Nginx pour Symfony 7 : front controller, statiques, et FastCGI “propre”
- PHP-FPM : dimensionner le pool pour Symfony (concurrence, latence, OOM)
- OPcache : réglages production, invalidation, preload (et les symlinks)
- Cache Symfony 7 : container, pools, warmup, et backends (Redis)
- Cache côté Nginx : microcache FastCGI (avec garde-fous) + compression
- Déploiement Symfony 7 reproductible : artefacts, rollback, et checklist opérable
Prérequis réalistes (versions, OS, runtime) et points de friction
On parle ici d’un déploiement Symfony 7.x derrière Nginx avec PHP-FPM, en environnement Linux type Debian 12 / Ubuntu 24.04 LTS, et un PHP moderne (dans la pratique : PHP 8.3 est un choix stable côté packaging et compatibilité). Symfony 7 impose un niveau de PHP élevé et pousse fortement vers des runtimes optimisés (container compilé, caches en prod, autoloader optimisé). Si vous êtes encore sur une base PHP « large » (extensions manquantes, limites mémoire trop basses, filesystem lent), le gain côté cache/opcache sera masqué par l’I/O.
Deux frictions “réalistes” à anticiper, surtout en Debian/Ubuntu :
- La disponibilité de PHP 8.3 : selon votre distribution et vos dépôts, PHP 8.3 peut ne pas être la version par défaut (ex. Debian “Bookworm” a longtemps livré une version inférieure). En production, fixez clairement votre stratégie (paquets distro, dépôt tiers, ou conteneur) pour éviter un serveur “patché à la main”.
- Les extensions PHP indispensables : Symfony et son écosystème font rapidement apparaître des dépendances implicites. En pratique, vérifiez au minimum :
opcache,intl,mbstring,xml,curl,zip,pdo_mysql(oupdo_pgsql), et si vous utilisez Redis :redis. Un oubli se manifeste souvent au pire moment (build en prod, ou route critique qui déclenche une classe non chargée).
Le prérequis que beaucoup sous-estiment : la compréhension du flux Nginx → FastCGI → PHP-FPM et de ses points de timeout/buffering. Si vous devez vous rafraîchir sur l’exécution réelle, l’article PHP-FPM : comprendre le flux d’exécution avec Nginx et Apache est un bon rappel pour éviter les erreurs classiques de SCRIPT_FILENAME, PATH_INFO ou de buffering FastCGI.
Un bon “sanity check” avant même de parler cache/opcache :
php -vetphp -m(extensions chargées) cohérents entre CLI et FPM (mêmes.ini, mêmes modules).- Limites process :
ulimit -n(file descriptors) trop bas peut provoquer des erreurs intermittentes sous charge (sockets, logs, connexions DB). - Stockage : si
var/(logs + cache) est sur un disque saturé, vos temps de réponse vont dériver. Sur VPS/instances (OVHcloud, Scaleway, Hetzner…), c’est souvent un goulot “invisible” : la CPU semble OK, mais l’I/O est le vrai plafond.
Enfin, si votre objectif est un déploiement “prod-grade” (latence stable, saturation CPU maîtrisée, pas de surprise à J+30), ne séparez pas « déploiement » et « hardening ». Les réglages OPcache et cache applicatif ne compensent pas une base serveur fragile (CVE non patchées, droits laxistes, secrets exposés). Sur ce point, la check-list Symfony : sécuriser un serveur PHP avec hardening et gestion CVE est directement pertinente.
Pour cadrer les timeouts (source fréquente de 502/504), gardez en tête l’alignement suivant :
| Couche | Paramètre | Rôle | Symptôme si mal réglé |
|---|---|---|---|
| Nginx | fastcgi_read_timeout |
temps max d’attente de la réponse FPM | 504 côté Nginx |
| PHP-FPM | request_terminate_timeout |
coupe un script trop long | 502/erreurs app si trop agressif |
| PHP | max_execution_time |
limite d’exécution script (selon SAPI) | erreurs “maximum execution time” |
Nginx pour Symfony 7 : front controller, statiques, et FastCGI “propre”
Le socle, c’est un vhost Nginx qui pointe uniquement sur public/ (pas sur la racine projet), et qui force le front controller index.php via try_files. En Symfony 7, vous voulez que Nginx serve les assets statiques sans réveiller PHP, et que tout le reste passe par PHP-FPM. Un try_files mal écrit se traduit en 404 fantômes, routes qui ne matchent plus, ou pire : exposition de fichiers .env / composer.json si vous avez un root trop haut.
Voici un exemple minimaliste, compatible avec un déploiement par symlink current (release-based). Le point important : utiliser $realpath_root pour que Nginx résolve le chemin final (sinon OPcache peut multiplier les entrées à cause des chemins symlinkés et vous perdez en hit ratio). Concrètement, avec des releases du type /releases/20260814120000/ puis un current -> releases/..., $realpath_root évite que le même fichier soit vu sous plusieurs chemins (et donc compilé/caché plusieurs fois).
server {
listen 443 ssl http2;
server_name example.tld;
root /var/www/app/current/public;
index index.php;
# Sécurité basique : jamais de fichiers cachés, ni de vendor, ni de config
location ~* /(\.env|\.git|composer\.(json|lock)|symfony\.lock|vendor/|var/) { return 404; }
# Assets statiques
location ~* \.(?:css|js|map|jpg|jpeg|png|webp|svg|ico|woff2?)$ {
access_log off;
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
try_files $uri =404;
}
# Front controller
location / {
try_files $uri /index.php$is_args$args;
}
location ~ ^/index\.php(/|$) {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_pass unix:/run/php/php8.3-fpm-app.sock;
fastcgi_read_timeout 60s;
# Buffering FastCGI: utile pour éviter que Nginx n’écrive trop sur disque en cas de grosses réponses
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
internal;
}
# Bloque l’accès direct à index.php si le bloc ci-dessus n’a pas été matché
location ~ \.php$ { return 404; }
}
Deux ajouts pratiques qu’on voit souvent en production (sans alourdir la config) :
- Uploads : si votre application gère du upload (formulaire, back-office), ajustez
client_max_body_size(sinon vous aurez des 413 difficiles à diagnostiquer côté utilisateur). - Headers reverse-proxy : si vous êtes derrière un load balancer (HAProxy, Nginx en frontal, Cloud provider), assurez-vous que Symfony voit le bon schéma/host (sinon génération d’URLs en HTTP, cookies “secure” incohérents, etc.). Ça se traite via la conf Symfony (trusted proxies) + headers corrects.
TLS et headers de sécurité ne sont pas le sujet central ici, mais ne faites pas l’impasse : un déploiement “rapide” qui casse l’HTTPS (HSTS mal posé, chaîne incomplète) vous plombe à la fois SEO et UX. Pour la partie audit HTTPS, l’article HTTPS SEO : bonnes pratiques, certificats SSL et audit technique couvre bien les pièges courants.
PHP-FPM : dimensionner le pool pour Symfony (concurrence, latence, OOM)
Avec Symfony 7, la charge typique en prod, c’est : pics de concurrence courts, templates Twig et container déjà compilés, et un coût I/O (sessions, cache, logs) qui devient dominant si vous ne le traitez pas. Le paramètre “magique” n’existe pas : vous dimensionnez pm.max_children sur la mémoire par worker et vous ajustez selon la latence et le taux de 502/504.
Méthode simple et défendable : mesurez la RSS moyenne d’un worker PHP sur une page représentative (profil prod, APP_DEBUG=0, OPcache actif). Exemple réaliste : 60–90 MiB/worker (ça varie selon extensions, intl, taille du container, charge). Si vous avez 8 GiB de RAM, et que vous réservez 2 GiB pour l’OS + Nginx + marge, vous avez ~6 GiB pour PHP. À 75 MiB/worker, pm.max_children ≈ 6144/75 ≈ 81. Cette approche évite le “ça passe sur ma VM” suivi d’un OOM killer en prod.
Pour rendre ce calcul opérable, vous pouvez vous donner une règle “rapide” :
| RAM machine | RAM dispo pour PHP (ex. -25%) | RSS/worker | pm.max_children approx. |
|---|---|---|---|
| 4 GiB | 3 GiB | 70 MiB | ~43 |
| 8 GiB | 6 GiB | 75 MiB | ~81 |
| 16 GiB | 12 GiB | 85 MiB | ~144 |
Astuce exploitation : la RSS n’est pas constante. Mesurez en charge (ou au moins après quelques minutes de trafic) et gardez 10–20% de marge.
Exemple de pool (Debian/Ubuntu) :
; /etc/php/8.3/fpm/pool.d/app.conf
[app]
user = www-data
group = www-data
listen = /run/php/php8.3-fpm-app.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 80
pm.start_servers = 12
pm.min_spare_servers = 8
pm.max_spare_servers = 20
pm.max_requests = 1000
; Observabilité
pm.status_path = /fpm-status
ping.path = /fpm-ping
request_slowlog_timeout = 2s
slowlog = /var/log/php8.3-fpm/app-slow.log
; Timeouts défensifs
request_terminate_timeout = 60s
; Laisser l’appli gérer les variables via dump-env / secrets
clear_env = yes
Trois points souvent ignorés : (1) pm.max_requests limite les fuites mémoire et la fragmentation (utile avec certaines extensions), (2) request_slowlog_timeout + slowlog vous donnent un signal rapide sur une régression (Doctrine qui part en N+1, appel API externe bloquant), (3) status_path permet de corréler saturation et latence.
En complément, deux réglages “anti-surprise” courants :
catch_workers_output = yes(dans le masterphp-fpm.confou pool) si vous voulez remonter certainserror_logvers journald/syslog (utile quand l’appli écrit sur stderr).- Un
memory_limitcohérent (souvent enphp_admin_value[memory_limit]) : trop bas = erreurs fatales sous charge ; trop haut = moins de workers possibles.
Pour la stack Nginx/PHP-FPM complète (y compris Redis/MariaDB) dans un contexte e-commerce, la logique de dimensionnement est similaire à celle décrite dans Serveur dédié PrestaShop : config Nginx PHP-FPM MariaDB Redis.
OPcache : réglages production, invalidation, preload (et les symlinks)
OPcache est non négociable en prod. Le rappel officiel est limpide :
“OPcache improves PHP performance by storing precompiled script bytecode in shared memory…” — PHP Manual, OPcache (https://www.php.net/manual/en/book.opcache.php)
Concrètement : sans OPcache, chaque requête reparcourt le parsing/compilation de fichiers PHP (même si le filesystem cache aide), ce qui explose la latence CPU sous charge. Avec OPcache correctement dimensionné, vous stabilisez le TTFB et vous réduisez la variance, surtout quand vous scalez horizontalement (moins de “cold starts” applicatifs).
Base recommandée (à ajuster selon taille du codebase + vendor) :
; /etc/php/8.3/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=100000
opcache.validate_timestamps=0
opcache.revalidate_freq=0
opcache.save_comments=1
opcache.fast_shutdown=1
; Optionnel: preload (voir plus bas)
; opcache.preload=/var/www/app/current/config/preload.php
; opcache.preload_user=www-data
Le point qui casse les déploiements : opcache.validate_timestamps=0 signifie “je ne revalide jamais les fichiers”. C’est ce qu’on veut en prod pour la perf, mais ça implique une règle d’exploitation : après chaque release, vous devez recharger PHP-FPM (graceful reload) pour purger OPcache, sinon vous servez de l’ancien bytecode. Avec un déploiement par symlink current, c’est encore plus critique : OPcache peut conserver l’ancien chemin résolu. La pratique robuste : systemctl reload php8.3-fpm (ou reload du service pool), puis éventuellement un healthcheck HTTP.
Checklist OPcache “anti-angle mort” :
- Surveillez les métriques : hit rate, memory usage, et wasted memory (fragmentation). Une
memory_consumptiontrop faible se traduit par des “restarts” internes et une perf qui varie. - Évitez de confondre OPcache et realpath cache : sur des codebases à beaucoup de fichiers,
realpath_cache_size/realpath_cache_ttlcôté PHP peuvent aussi influencer le coût d’accès au filesystem (moins visible, mais mesurable). - Ne sur-vendez pas le JIT : dans beaucoup d’applications web Symfony classiques (I/O + frameworks + DB), l’impact de JIT est souvent faible vs. une bonne config OPcache + FPM + DB.
Le preload est utile sur Symfony si vous avez une app avec beaucoup de classes chargées “souvent” (kernel, container, services critiques). Il ne faut pas en faire un dogme : c’est une optimisation micro/mesurée. Vous générez un script de preload simple qui requires les fichiers à charger (Symfony documente le principe) et vous observez le gain via un benchmark type wrk/vegeta. Sur certaines apps API, on voit typiquement un gain de quelques pourcents sur la latence P95, pas une révolution.
Cache Symfony 7 : container, pools, warmup, et backends (Redis)
Symfony 7 en prod doit tourner en APP_ENV=prod et APP_DEBUG=0. Ce n’est pas une “bonne pratique”, c’est une condition de performance et de sécurité, parce que le container compile, Twig compile, et les caches de métadonnées (routing, attributs) changent de comportement. La page officielle “Deployment” de Symfony détaille ces principes : https://symfony.com/doc/current/deployment.html
Le déploiement “propre” ne fait pas rm -rf var/cache au hasard. Il suit un enchaînement reproductible :
composer install --no-dev --prefer-dist --no-interaction --optimize-autoloadercomposer dump-env prod(génère.env.local.php; évite de parser.enven prod)bin/console cache:clear --env=prodpuisbin/console cache:warmup --env=prod- migrations (si besoin) avec un verrouillage / fenêtre de maintenance maîtrisée
Deux nuances utiles en pratique :
- Warmup “avant switch” : dans un modèle à releases, exécutez le warmup dans le répertoire de la nouvelle release avant de repointer le symlink
current. Sinon, votre premier utilisateur (ou votre robot) “paie” le cold start. - Permissions : les erreurs post-déploiement proviennent souvent de
var/non accessible à l’utilisateur FPM. Sur Debian/Ubuntu, un duo fréquent est deploy user +www-data. Des ACL (setfacl) ou une stratégie claire de ownership évitent les permissions qui “driftent”.
Le choix du backend de cache applicatif compte autant que le warmup. En 2026, Redis reste le standard de facto pour cache.app et cache.system dès que vous avez plus d’un nœud. En YAML (exemple) :
# config/packages/cache.yaml
framework:
cache:
app: cache.adapter.redis
default_redis_provider: '%env(REDIS_URL)%'
Ça règle deux problèmes : (1) cohérence de cache inter-instances, (2) pression réduite sur le disque (évite que var/cache devienne un hotspot I/O). Attention : mettez des TTL explicites sur vos pools métier, et ne confondez pas cache applicatif et “session store” (mêmes technos possibles, mais politiques différentes).
Mini-scénario concret (très courant) : vous mettez Redis pour cache.app, mais vous oubliez que certaines clés métier n’ont pas de TTL. Résultat : la mémoire Redis gonfle, l’éviction devient chaotique, et vos performances “oscillent” au fil des jours. Une politique simple (TTL par défaut + exceptions explicitement documentées) évite cette dérive.
Si vous voulez pousser la sécurité côté base de données en parallèle (très fréquent quand on industrialise un déploiement Symfony + MySQL/MariaDB), la méthodo Sécurité MySQL : pare-feu, TLS, privilèges minimaux et audit est applicable telle quelle.
Cache côté Nginx : microcache FastCGI (avec garde-fous) + compression
Symfony a son cache HTTP interne et peut s’intégrer avec des reverse proxies (Varnish, Nginx). Mais dans la vraie vie, beaucoup d’équipes veulent un gain immédiat sur des pages publiques très consultées : microcaching FastCGI (cacher 1–10 secondes) côté Nginx. Sur un endpoint public (landing, listing, page statique), ça lisse les spikes et ça protège PHP-FPM. Sur une API GET cacheable, même logique.
Exemple (à adapter) :
fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=PHPFASTCGI:100m inactive=60m max_size=2g;
map $http_cookie $skip_cache {
default 0;
~*"(PHPSESSID|sf_redirect|token|admin)" 1;
}
server {
# ...
location ~ ^/index\.php(/|$) {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_pass unix:/run/php/php8.3-fpm-app.sock;
fastcgi_cache PHPFASTCGI;
fastcgi_cache_valid 200 1s;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
add_header X-FastCGI-Cache $upstream_cache_status;
}
}
Trois avertissements, sinon vous allez vous tirer une balle : (1) ne microcachez pas des pages personnalisées (cookie, session, panier), (2) gérez explicitement les bypass sur endpoints sensibles, (3) vérifiez vos headers (Cache-Control, Vary) et testez avec un compte authentifié et un compte anonyme.
Deux garde-fous supplémentaires qui valent souvent le coup :
- Limiter au GET/HEAD (pas de cache sur POST/PUT…) : le plus simple est de bypass via une map sur
$request_method. - Éviter de cacher les réponses qui posent des cookies : si votre page anonyme renvoie parfois
Set-Cookie, vous risquez d’empoisonner le cache. Vous pouvez bypass si vous détectez certains cookies côté requête (comme ci-dessus) et, selon votre cas, empêcher le cache sur des routes spécifiques.
Pour la référence officielle des directives (comportement, clés, invalidation), la documentation Nginx du module FastCGI est l’endroit fiable : https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_cache
Ce mécanisme est très différent du cache Symfony : Nginx cache la réponse HTTP (full page), Symfony cache des données ou des fragments. Les deux peuvent coexister, mais il faut une stratégie claire (qu’est-ce qui est cacheable, combien de temps, et comment invalider).
Enfin, compression : activez gzip (et brotli si dispo) côté Nginx pour réduire le coût réseau, surtout sur JSON/HTML. Ce n’est pas spécifique à Symfony 7, mais si vous visez de la perf SEO (LCP/TTFB), c’est un levier direct. Exemple minimal (à adapter) :
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types
text/plain text/css application/javascript application/json
application/xml image/svg+xml;
gzip_vary on;
Pour un cadrage plus large “cache + CDN”, l’article CDN 2026 : critères techniques pour choisir le meilleur fournisseur peut servir de grille de lecture : un microcache local améliore les pics, un CDN réduit la latence géographique (utile si vos utilisateurs sont répartis France/Europe).
Déploiement Symfony 7 reproductible : artefacts, rollback, et checklist opérable
Si votre “déploiement” consiste à git pull sur le serveur puis composer install, vous cumulez : drift de dépendances, downtime aléatoire pendant le warmup, permissions qui divergent, et difficulté à rollback. Le pattern qui tient en prod : build d’artefact (CI), puis déploiement d’une release immuable, switch atomique par symlink, reload FPM, healthcheck, et seulement ensuite purge éventuelle de caches distribués.
Une séquence simple, éprouvée :
/var/www/app/releases/<timestamp>/contient le code + vendor (artefact)/var/www/app/currentpointe vers la release activeshared/contient ce qui doit survivre (uploads, clés, etc.)- post-deploy :
bin/console cache:warmup --env=prod,systemctl reload php8.3-fpm, puis check/healthz
Dans un contexte “Europe/France” (hébergement Paris/Gravelines/Strasbourg, ou régions équivalentes chez d’autres providers), l’effet le plus tangible n’est pas seulement la latence brute : c’est la stabilité. Un déploiement atomique + warmup en amont réduit les fenêtres où les utilisateurs subissent des temps de réponse erratiques (souvent visibles sur les métriques SEO et dans les pics de taux de rebond).
Le rollback devient trivial : repointer current vers l’ancienne release + reload FPM. Ce modèle se transpose très bien en conteneurs et en GitOps (où le “rollback” est un revert de version d’image). Si vous êtes déjà dans une trajectoire Kubernetes, la checklist Migration PrestaShop vers Kubernetes : checklist GitOps et pièges courants est étonnamment applicable aux apps Symfony (sondes, migrations, secrets, ressources).
Une micro-checklist de contrôles “avant switch” qui évite beaucoup d’incidents :
bin/console about --env=prod(versions, env, debug off)bin/console cache:warmup --env=prodsans erreurphp-fpm8.3 -t(ou équivalent) etnginx -tdans la CI / pré-prodcurl -fsS https://example.tld/healthzretourne 200 (et idéalement vérifie DB/Redis)
Checklist finale (celle qui évite les incidents bêtes) :
APP_ENV=prod,APP_DEBUG=0,.envnon exposé,composer dump-env prodexécuté.- Nginx
root→public/uniquement, blocage des chemins sensibles,try_filescorrect. - PHP-FPM pool dimensionné sur mémoire mesurée,
slowlogactif,pm.status_pathaccessible en interne. - OPcache activé,
validate_timestamps=0+ procédure de reload FPM à chaque release. cache:warmupexécuté hors trafic (ou sur nouvelle release avant switch), et backend Redis si multi-instances.- Microcache Nginx seulement sur endpoints explicitement “public/anonymous”, avec bypass cookies.
Si malgré tout vous tombez sur une “page blanche” post-déploiement, ne perdez pas 2h à tâtonner : commencez par logs PHP-FPM + APP_DEBUG=0 + erreurs 500/502, puis vérifiez OPcache/reload et permissions var/. Les méthodes de diagnostic détaillées dans PrestaShop page blanche : diagnostiquer l’erreur fatale PHP rapidement s’appliquent quasiment à l’identique sur Symfony : même logique “logs d’abord, hypothèses ensuite”, surtout quand OPcache et les symlinks entrent en jeu.




