Skip to Content

Chapitre 10.1 — PHP-FPM

⏱️ TL;DR — En prod moderne, on ne fait plus tourner PHP « dans » le serveur web (le vieux mod_php d’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 installes php-fpm, tu écris un location ~ \.php$ qui fait fastcgi_pass vers 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 .php vers FPM.
  • Régler un pool (dynamic vs static, 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_pass vers 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*-fpm

Le 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 : 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_root est ton root, $fastcgi_script_name la partie chemin de l’URL.

⚠️ Piège — Oublier SCRIPT_FILENAME (ou le laisser incohérent avec root) 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 un 404 FastCGI alors que le fichier existe bien, vérifie en premier que SCRIPT_FILENAME pointe 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 nginx

Les 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 = 500

Les modes de pm :

ModeComportementQuand
dynamicFPM ajuste le nombre de workers entre min et max selon la chargeLe défaut sain, la plupart des cas
staticNombre fixe de workers (= pm.max_children), toujours làCharge élevée et stable, latence prévisible
ondemandAucun worker au repos, créés à la demandePetits 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_children trop 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-fpm

OPcache : 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.sock

Il 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 .php uploadé par un attaquant ne doit jamais s’exécuter. (2) Régler cgi.fix_pathinfo=0 dans php.ini pour é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.conf livré avec le paquet, très pédagogiques. Pour Nginx + FastCGI, la doc du module ngx_http_fastcgi_module. Ne recopie pas un www.conf trouvé 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 pool www dédié à l’API (user www-data, socket /run/php/php8.3-fpm.sock), avec OPcache activé et pm = dynamic, pm.max_children calé sur la RAM réservée à PHP après que Postgres et le front Next.js ont pris leur part. Le server block Nginx de api.formacampus.fr a exactement la forme vue plus haut, root sur public/. 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 ?

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 ?

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 ?

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 ?

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.

Last updated on