Chapitre 10.1 — PHP-FPM
⏱️ TL;DR — En prod moderne, on ne fait plus tourner PHP « dans » le serveur web (le vieux
mod_phpd’Apache). On fait tourner un process séparé, PHP-FPM (FastCGI Process Manager), et Nginx lui parle en FastCGI. Le modèle mental est le même que pour Node : Nginx en façade, PHP-FPM en local, sauf que le canal n’est pas du HTTP proxifié mais du FastCGI, souvent via un socket Unix (/run/php/phpX.Y-fpm.sock). Tu installesphp-fpm, tu écris unlocation ~ \.php$qui faitfastcgi_passvers ce socket, tu règles le pool (pm,pm.max_children) selon ta RAM, tu actives OPcache, et PHP tourne vite et proprement. Ce chapitre pose les fondations réutilisées par Symfony (10.2) et WordPress (10.3).
🎯 Objectifs
- Comprendre pourquoi PHP-FPM a remplacé
mod_php, et ce qu’est FastCGI. - Installer PHP-FPM et repérer le socket du pool.
- Écrire le server block Nginx qui relaie les fichiers
.phpvers FPM. - Régler un pool (
dynamicvsstatic,pm.max_children) selon la RAM disponible. - Activer OPcache et savoir gérer plusieurs versions de PHP sur une machine.
PHP en prod : pas mod_php, mais FPM
Historiquement, PHP tournait à l’intérieur d’Apache via le module mod_php : chaque process Apache embarquait un interpréteur PHP. Simple, mais lourd — l’interpréteur est chargé même pour servir une image, et Apache et PHP sont soudés. Le monde a basculé vers une architecture découplée : un serveur web léger (Nginx) en façade, et un gestionnaire de process PHP dédié derrière. Ce gestionnaire, c’est PHP-FPM.
FPM lance et supervise un pool de workers PHP qui attendent du travail. Quand Nginx reçoit une requête pour un .php, il ne l’exécute pas lui-même : il passe le relais à FPM en parlant FastCGI, un protocole binaire fait pour ça. FPM exécute le script, renvoie la réponse à Nginx, qui la sert au visiteur.
💡 Réflexe — Grave le modèle : « Nginx parle FastCGI à FPM. » Nginx ne « comprend » pas PHP ; il délègue. C’est exactement l’idée du reverse proxy de la Partie 7, avec un autre protocole entre les deux. Si tu as saisi
proxy_passvers Node, tu as déjà 90 % de PHP-FPM.
Installer PHP-FPM
Sur Ubuntu/Debian, le paquet de base est php-fpm. Il tire la version de PHP par défaut des dépôts. On ajoute les extensions dont l’appli a besoin (base de données, mbstring, xml, etc.).
sudo apt update
# Le moteur FPM + les extensions courantes d'une app Symfony ou WordPress
sudo apt install php-fpm php-cli php-mysql php-xml php-mbstring php-curl php-zip php-intl php-gd
# Vérifie la version installée
php -v
# Le service FPM tourne-t-il ?
systemctl status php*-fpmLe nom du service et du socket inclut le numéro de version (par ex. php8.3-fpm). Comme les versions évoluent, on écrit ici phpX.Y : remplace par ce que php -v t’affiche.
🐚 Au terminal — Pour trouver le chemin exact du socket de ton pool sans deviner :
# Liste les sockets FPM présents (le nom porte la version)
ls -l /run/php/
# -> ex. /run/php/php8.3-fpm.sock
# Où est la config du pool par défaut ?
sudo cat /etc/php/*/fpm/pool.d/www.conf | grep -E "^(listen|user|pm)"Le socket vit typiquement dans /run/php/phpX.Y-fpm.sock. C’est un socket Unix (un fichier spécial), pas un port TCP : plus rapide et non exposé au réseau, ce qui est parfait puisque seul Nginx, sur la même machine, a besoin de parler à FPM.
Le server block Nginx qui parle à FPM
Voici le cœur du chapitre : le server block. La logique — servir les fichiers existants tels quels, et router tout le dynamique vers un index.php qui passe à FPM.
server {
listen 80;
server_name exemple.fr;
# La racine web : le dossier public de l'appli PHP
root /var/www/exemple/public;
index index.php;
# Sert le fichier s'il existe, sinon passe la main au front controller
location / {
try_files $uri /index.php$is_args$args;
}
# Toute requête vers un .php part en FastCGI vers FPM
location ~ \.php$ {
# Le socket du pool (adapte la version)
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
# Les paramètres FastCGI standards fournis par Nginx
include fastcgi_params;
# LE paramètre crucial : quel fichier PHP exécuter, en chemin absolu
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Interdit d'exécuter du PHP planqué dans les dossiers d'upload
location ~ /uploads/.*\.php$ {
deny all;
}
}Décortiquons les directives qui comptent :
root: le dossier public de l’appli. On y reviendra en 10.2, mais retiens : on ne pointe jamais la racine web sur le code source, seulement sur le dossier exposable.location ~ \.php$: le~déclenche une correspondance par expression régulière.\.php$veut dire « se termine par.php». Toute URL de ce type part en FastCGI.fastcgi_pass unix:/run/php/php8.3-fpm.sock: où envoyer la requête FastCGI — ici le socket Unix du pool.include fastcgi_params: Nginx charge un fichier tout prêt qui remplit une flopée de variables FastCGI (REQUEST_METHOD,QUERY_STRING, en-têtes…). Ne le réécris pas à la main.fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name: le paramètre à ne jamais oublier. Il dit à FPM quel fichier exécuter, en chemin absolu sur le disque.$document_rootest tonroot,$fastcgi_script_namela partie chemin de l’URL.
⚠️ Piège — Oublier
SCRIPT_FILENAME(ou le laisser incohérent avecroot) donne des erreurs déroutantes : « File not found » renvoyé par FPM, ou une page blanche. FPM cherche un fichier au mauvais endroit. Si tu vois « No input file specified » ou un404FastCGI alors que le fichier existe bien, vérifie en premier queSCRIPT_FILENAMEpointe le bon chemin absolu.
Après toute modif : on teste, puis on recharge.
sudo nginx -t # valide la syntaxe (JAMAIS reload sans ce test)
sudo systemctl reload nginxLes pools FPM : www.conf et le tuning mémoire
Un pool est un groupe de workers PHP partageant une config. Le pool par défaut se configure dans /etc/php/X.Y/fpm/pool.d/www.conf. Deux réglages structurent tout : le mode de gestion des process (pm) et le plafond de workers (pm.max_children).
; /etc/php/8.3/fpm/pool.d/www.conf (extrait)
user = www-data
group = www-data
; Le socket écouté (doit matcher le fastcgi_pass de Nginx)
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
; Mode de gestion des workers
pm = dynamic
; Plafond ABSOLU de workers simultanés — le réglage le plus important
pm.max_children = 10
; Workers prêts au démarrage
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 4
; Recycle un worker après N requetes (évite les fuites mémoire)
pm.max_requests = 500Les modes de pm :
| Mode | Comportement | Quand |
|---|---|---|
dynamic | FPM ajuste le nombre de workers entre min et max selon la charge | Le défaut sain, la plupart des cas |
static | Nombre fixe de workers (= pm.max_children), toujours là | Charge élevée et stable, latence prévisible |
ondemand | Aucun worker au repos, créés à la demande | Petits sites, VPS à faible RAM |
Le calcul de pm.max_children est une histoire de RAM, pas de magie. Un worker PHP consomme de la mémoire (variable selon l’appli, souvent quelques dizaines de Mo). La règle de dos de nappe :
pm.max_children ≈ (RAM disponible pour PHP) / (RAM moyenne d'un worker)⚠️ Piège — Mettre
pm.max_childrentrop haut est pire que trop bas. Sous un pic de trafic, FPM lance tous ces workers, la RAM sature, le noyau se met à swapper (voire l’OOM-killer tue des process) et le serveur s’écroule au moment où il faudrait tenir. Mieux vaut un plafond honnête qui met les requêtes en file d’attente qu’un plafond fantaisiste qui fait tomber la machine. Mesure la RAM d’un worker (ps,htop) avant de fixer le chiffre.
Après modif du pool, on recharge FPM (pas Nginx) :
sudo systemctl reload php8.3-fpmOPcache : le gain gratuit
Par défaut, PHP recompile chaque script à chaque requête. OPcache garde le bytecode compilé en mémoire : le même script n’est compilé qu’une fois. C’est souvent un facteur 2 ou plus sur les perfs, pour zéro effort. L’extension est présente ; il suffit de l’activer/régler dans le php.ini de FPM.
; /etc/php/8.3/fpm/conf.d/10-opcache.ini (ou dans php.ini)
opcache.enable=1
opcache.memory_consumption=128 ; Mo de mémoire pour le cache
opcache.max_accelerated_files=20000 ; nb de fichiers cachés (monte pour un gros framework)
opcache.validate_timestamps=1 ; en prod stricte : 0, et on vide le cache au déploiement💡 Réflexe — En prod,
opcache.validate_timestamps=0évite à PHP de vérifier la date de chaque fichier à chaque requête (plus rapide). Contrepartie : après un déploiement, le cache ne se rafraîchit pas tout seul — il faut recharger FPM (systemctl reload phpX.Y-fpm) pour qu’il relise le code. On l’automatisera dans le déploiement (Partie 13).
Plusieurs versions de PHP
Un VPS peut héberger un vieux WordPress en PHP 7.x et une API Symfony en PHP 8.x. C’est possible : chaque version a son propre service FPM et son propre socket. Sur Ubuntu, on passe souvent par le PPA d’Ondřej Surý pour installer plusieurs versions côte à côte.
# Deux versions installées => deux sockets distincts
ls /run/php/
# php8.1-fpm.sock php8.3-fpm.sockIl suffit alors, dans chaque server block, de faire pointer fastcgi_pass vers le bon socket. Le site A parle à php8.1-fpm.sock, le site B à php8.3-fpm.sock. Rien d’autre à changer : le patron reste identique.
🔒 Sécurité — Trois durcissements FPM à connaître. (1) Interdire l’exécution de PHP dans les dossiers d’upload (
location ~ /uploads/.*\.php$ { deny all; }) : un fichier.phpuploadé par un attaquant ne doit jamais s’exécuter. (2) Réglercgi.fix_pathinfo=0dansphp.inipour éviter que FPM exécute un mauvais fichier via un chemin bidouillé. (3) Faire tourner chaque site sous son propre pool avec un user dédié : si un site est compromis, il ne peut pas lire les fichiers du voisin. L’isolation par pool est le vrai atout de FPM face àmod_php.
📚 La doc — La référence FPM est la doc PHP officielle (« FastCGI Process Manager (FPM) ») et les commentaires du
www.conflivré avec le paquet, très pédagogiques. Pour Nginx + FastCGI, la doc du modulengx_http_fastcgi_module. Ne recopie pas unwww.conftrouvé sur un blog : lis celui de ta version.
🧭 Sur FormaCampus — L’API Symfony de FormaCampus est déployée en PHP-FPM derrière Nginx sur
api.formacampus.fr. Le VPS Ubuntu LTS fait tourner un poolwwwdédié à l’API (userwww-data, socket/run/php/php8.3-fpm.sock), avec OPcache activé etpm = dynamic,pm.max_childrencalé sur la RAM réservée à PHP après que Postgres et le front Next.js ont pris leur part. Le server block Nginx deapi.formacampus.fra exactement la forme vue plus haut,rootsurpublic/. Le détail Symfony (cache, permissions, migrations) est le sujet du chapitre suivant.
✏️ Exercices
Exercice 1 — Écris le server block. Une petite appli PHP est déployée dans /var/www/facture/public, son front controller est index.php, et la machine fait tourner PHP 8.3. Écris le server block Nginx minimal (écoute en 80) qui sert le statique et relaie le PHP vers FPM.
✅ Solution
server {
listen 80;
server_name facture.exemple.fr;
root /var/www/facture/public;
index index.php;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}Points clés : root sur public/, try_files qui retombe sur le front controller, location ~ \.php$ avec le bon socket, include fastcgi_params et surtout SCRIPT_FILENAME. On finit par sudo nginx -t && sudo systemctl reload nginx.
Exercice 2 — Débogue le « File not found ». Tu ouvres le site, Nginx répond mais tu obtiens une page « No input file specified ». Le fichier index.php existe pourtant. Quelles sont les deux causes les plus probables et comment tu les vérifies ?
✅ Solution
Cette erreur vient de FPM qui ne trouve pas le fichier à exécuter. Causes les plus fréquentes : (1) SCRIPT_FILENAME mal formé ou root incorrect — le chemin absolu calculé ne pointe pas le vrai fichier ; vérifie root et la ligne fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name. (2) Permissions : l’utilisateur de FPM (www-data) ne peut pas lire le dossier ; vérifie avec ls -l et sudo -u www-data cat /var/www/.../index.php. Vérifie aussi que le socket dans fastcgi_pass correspond bien à celui du pool (listen dans www.conf).
Exercice 3 — Dimensionne le pool. Ton VPS a 2 Go de RAM. Postgres et l’OS en prennent ~700 Mo, tu réserves ~1 Go à PHP. Tu mesures qu’un worker consomme en moyenne 60 Mo. Quel pm.max_children retiens-tu, et pourquoi pas le double ?
✅ Solution
1000 Mo / 60 Mo ≈ 16 → on cale pm.max_children autour de 15 (on garde une marge). Doubler à 32 dépasserait la RAM réservée : sous charge, FPM lancerait tous ces workers, la machine swapperait ou l’OOM-killer tuerait des process — le serveur tombe pile au pic de trafic. Un plafond réaliste met les requêtes en file d’attente plutôt que d’écrouler la machine. On mesure la RAM réelle d’un worker avant de figer le chiffre.
🧠 Quiz de révision
1. Pourquoi utilise-t-on PHP-FPM plutôt que l’ancien mod_php ?
mod_php ?Parce que FPM découple PHP du serveur web : Nginx (léger) sert en façade et délègue le PHP à un pool de workers dédiés et supervisés. On gagne en perf (Nginx ne porte pas l’interpréteur), en isolation (un pool + un user par site), et en souplesse (plusieurs versions de PHP côte à côte).
2. Quel protocole Nginx utilise-t-il pour parler à PHP-FPM, et par quel canal ?
FastCGI, généralement via un socket Unix (/run/php/phpX.Y-fpm.sock). C’est différent du proxy_pass HTTP utilisé pour Node : ici c’est fastcgi_pass vers le socket du pool.
3. À quoi sert le paramètre SCRIPT_FILENAME et que se passe-t-il s’il est faux ?
SCRIPT_FILENAME et que se passe-t-il s’il est faux ?Il indique à FPM quel fichier PHP exécuter, en chemin absolu ($document_root$fastcgi_script_name). S’il est faux ou incohérent avec root, FPM ne trouve pas le fichier : erreurs « File not found » / « No input file specified » ou page blanche, même si le fichier existe.
4. Que règle pm.max_children et quel est le risque de le mettre trop haut ?
pm.max_children et quel est le risque de le mettre trop haut ?Le nombre maximum de workers PHP simultanés. Trop haut, sous un pic FPM lance plus de workers que la RAM ne peut en tenir : la machine swappe ou l’OOM-killer frappe, et le serveur tombe. On le calcule à partir de la RAM réservée à PHP divisée par la RAM moyenne d’un worker.
5. Qu’apporte OPcache et que ne faut-il pas oublier après un déploiement si validate_timestamps=0 ?
validate_timestamps=0 ?OPcache garde le bytecode compilé en mémoire : chaque script n’est compilé qu’une fois, gros gain de perf. Avec validate_timestamps=0, PHP ne revérifie plus les fichiers ; après un déploiement il faut donc recharger FPM (systemctl reload phpX.Y-fpm) pour que le nouveau code soit pris en compte.
Chapitre suivant : Symfony en prod — déployer une appli Symfony proprement : APP_ENV=prod, cache, permissions, public/ et OPcache.