Chapitre 417 minIntermédiaire
PHP-FPM et Nginx : installer la pile LEMP sur Debian
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- expliquer comment Nginx fait exécuter un script PHP ;
- installer PHP 8.4 et PHP-FPM sans installer Apache par erreur ;
- créer un pool PHP-FPM par application, avec son compte ;
- régler PHP pour la production et dimensionner un pool ;
- diagnostiquer une erreur 502, 504 ou « File not found ».
3 exercices corrigés1 schéma
PHP-FPM (FastCGI Process Manager) est le service qui exécute les scripts PHP sur un serveur web : Nginx lui transmet chaque requête vers un fichier .php par le protocole FastCGI, PHP-FPM exécute le script et renvoie la page produite. Avec une base MariaDB, cela forme la pile LEMP (Linux, Nginx, MariaDB ou MySQL, PHP), cousine de la pile LAMP qui utilise Apache. Ce chapitre installe PHP 8.4 et PHP-FPM sur Debian 13, relie Nginx à PHP-FPM, crée un pool par application et règle PHP pour la production.
Comment Nginx exécute du PHP
Nginx ne sait pas exécuter de PHP. Il sert lui-même les fichiers statiques (images, feuilles de style, scripts JavaScript) et confie les fichiers .php à PHP-FPM, un service séparé, à l'écoute sur un socket Unix (un fichier spécial, /run/php/php8.4-fpm.sock, qui relie deux processus de la même machine) ou sur un port TCP.
Séparer le serveur web de PHP a trois avantages :
- la sécurité : chaque application peut tourner sous son propre compte, dans son propre pool de processus, sans pouvoir lire les fichiers des autres ;
- les ressources : Nginx garde ses milliers de connexions avec peu de mémoire, et seuls les processus PHP nécessaires tournent ;
- la maintenance : tu peux redémarrer PHP-FPM sans couper Nginx, ou faire tourner deux versions de PHP côte à côte.
Installer PHP 8.4 et PHP-FPM
Debian 13 fournit PHP 8.4. Installe PHP-FPM et les extensions dont l'application a besoin, en nommant les paquets avec leur version :
sudo apt install php8.4-fpm php8.4-cli php8.4-curl php8.4-mbstring php8.4-intl php8.4-xml
php -v
# PHP 8.4.x (cli) …
systemctl status php8.4-fpm --no-pager
Les extensions courantes, toutes dans les dépôts de Debian :
| Paquet | Extension | Pour |
|---|---|---|
php8.4-mysql | mysqli, pdo_mysql | Se connecter à MariaDB ou MySQL |
php8.4-curl | curl | Appeler une API, télécharger |
php8.4-mbstring | mbstring | Chaînes accentuées (UTF-8) |
php8.4-intl | intl | Dates, nombres, langues |
php8.4-xml | dom, simplexml, xmlreader, xmlwriter | Lire et écrire du XML |
php8.4-gd | gd | Images, graphiques |
php8.4-zip, php8.4-bz2 | zip, bz2 | Archives |
php8.4-ldap | ldap | Annuaire LDAP ou Active Directory |
php8.4-bcmath | bcmath | Calculs en grande précision |
php8.4-opcache | OPcache | Cache du code compilé, installé d'office avec PHP-FPM |
php-fpm8.4 -m | grep -i -E 'mysqli|curl|opcache' # extensions chargées par PHP-FPM
PHP en ligne de commande (php) et PHP-FPM ont chacun leur fichier php.ini : /etc/php/8.4/cli/php.ini et /etc/php/8.4/fpm/php.ini. Un réglage modifié dans le premier ne change rien au site web : c'est la cause d'innombrables « j'ai pourtant changé la valeur ».
La configuration de PHP-FPM
| Fichier | Rôle |
|---|---|
/etc/php/8.4/fpm/php-fpm.conf | Réglages globaux du service (journal, PID) |
/etc/php/8.4/fpm/pool.d/*.conf | Un fichier par pool : un groupe de processus avec son compte, son socket et ses limites |
/etc/php/8.4/fpm/php.ini | Les réglages de PHP pour les scripts exécutés par PHP-FPM |
/etc/php/8.4/fpm/conf.d/ | L'activation des extensions (liens vers /etc/php/8.4/mods-available/) |
/var/log/php8.4-fpm.log | Le journal du service (démarrages, pools saturés) |
Le pool par défaut, www.conf, contient ces directives (sans les commentaires) :
[www]
user = www-data
group = www-data
listen = /run/php/php8.4-fpm.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
| Directive | Rôle |
|---|---|
user, group | Le compte sous lequel les scripts PHP s'exécutent |
listen | Le socket (ou 127.0.0.1:9000 pour un port TCP) sur lequel le pool attend Nginx |
listen.owner, listen.group | Le propriétaire du socket : Nginx (www-data) doit pouvoir y écrire |
pm | La gestion des processus : static (nombre fixe), dynamic (entre un minimum et un maximum), ondemand (créés à la demande, arrêtés après inactivité) |
pm.max_children | Le nombre maximal de scripts exécutés en même temps |
pm.start_servers, pm.min_spare_servers, pm.max_spare_servers | Le nombre de processus au démarrage et en attente (mode dynamic) |
Dimensionner pm.max_children. Chaque processus PHP occupe de la mémoire, souvent 30 à 80 Mo selon l'application. Si srv-web a 2 Go de mémoire, dont 1 Go disponible pour PHP, et qu'un processus en consomme 50 Mo, pm.max_children ne doit pas dépasser 1024 / 50, soit 20. Au-delà, le serveur manque de mémoire sous la charge. En dessous de la charge réelle, les requêtes attendent, et le journal le signale : WARNING: [pool www] server reached pm.max_children setting (5), consider raising it.
ps -o rss,cmd -C php-fpm8.4 # mémoire résidente de chaque processus, en kilo-octets
Relier Nginx à PHP-FPM
Dans le server block du site de suivi, un bloc location envoie les fichiers .php à PHP-FPM. Debian fournit le fragment snippets/fastcgi-php.conf, qui contient les réglages habituels :
server {
listen 80;
listen [::]:80;
server_name suivi.sorbier-logistique.fr;
root /var/www/suivi/public;
index index.php index.html;
access_log /var/log/nginx/suivi.access.log;
error_log /var/log/nginx/suivi.error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}
location ~ /\. {
deny all;
}
}
Le fragment fastcgi-php.conf découpe l'adresse en nom de script et en suite de chemin, vérifie que le script existe avant de le transmettre (try_files $fastcgi_script_name =404;), puis inclut fastcgi.conf, qui transmet à PHP les paramètres de la requête, dont SCRIPT_FILENAME, le chemin complet du script à exécuter. La vérification de l'existence du script n'est pas un détail : sans elle, une adresse comme /uploads/photo.jpg/x.php pourrait faire exécuter comme du PHP une image envoyée par un utilisateur.
La ligne try_files … /index.php?$query_string du bloc / renvoie vers index.php toute adresse qui ne correspond à aucun fichier : c'est le contrôleur frontal des applications modernes, qui lit l'adresse et appelle le bon code (cours PHP et MySQL).
Teste avec un script minimal, puis supprime-le :
echo '<?php echo "PHP ", PHP_VERSION, " via ", php_sapi_name(), "\n";' | sudo tee /var/www/suivi/public/test.php
sudo nginx -t && sudo systemctl reload nginx
curl -H "Host: suivi.sorbier-logistique.fr" http://192.168.10.225/test.php
# PHP 8.4.x via fpm-fcgi
sudo rm /var/www/suivi/public/test.php
Un pool par application
Avec le seul pool www, toutes les applications du serveur s'exécutent sous www-data : une faille dans l'une donne accès aux fichiers et aux secrets de toutes. La bonne pratique est un pool par application, chacun avec son compte. Pour le site de suivi :
sudo adduser --system --group --no-create-home --home /var/www/suivi suivi
Puis /etc/php/8.4/fpm/pool.d/suivi.conf :
[suivi]
user = suivi
group = suivi
listen = /run/php/php8.4-fpm-suivi.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 30s
pm.max_requests = 500
; Limiter PHP au dossier de l'application et au dossier temporaire
php_admin_value[open_basedir] = /var/www/suivi:/tmp
php_admin_flag[display_errors] = off
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /var/log/php-fpm/suivi.error.log
sudo mkdir -p /var/log/php-fpm && sudo chown suivi:suivi /var/log/php-fpm
sudo php-fpm8.4 -t # test de la configuration
sudo systemctl reload php8.4-fpm
ls -l /run/php/
# srw-rw---- 1 www-data www-data 0 … php8.4-fpm-suivi.sock
Dans le server block, remplace le socket par celui du pool : fastcgi_pass unix:/run/php/php8.4-fpm-suivi.sock;.
| Réglage | Effet |
|---|---|
user = suivi | Les scripts du site ne peuvent écrire que là où le compte suivi en a le droit |
listen.owner = www-data | Nginx, seul, peut parler au pool |
pm = ondemand | Aucun processus quand le site est inactif, jusqu'à 10 sous la charge |
pm.max_requests = 500 | Chaque processus est remplacé après 500 requêtes, ce qui limite l'effet d'une fuite de mémoire |
php_admin_value[open_basedir] | PHP refuse d'ouvrir un fichier hors de ces dossiers (/etc/passwd, les fichiers d'un autre site) |
php_admin_… | Une valeur imposée par l'administrateur, que le code de l'application ne peut pas modifier avec ini_set() |
Les fichiers du site appartiennent toujours à root et restent lisibles par tous ; seul le dossier où l'application écrit (un dossier var/ ou uploads/) appartient à suivi. Un attaquant qui exploiterait une faille ne pourrait donc pas modifier le code.
Régler PHP pour l'application
Les valeurs de /etc/php/8.4/fpm/php.ini s'appliquent à tous les pools. Celles qu'on ajuste le plus souvent :
expose_php = Off ; ne pas annoncer « X-Powered-By: PHP/8.4.x » dans les réponses
display_errors = Off ; jamais d'erreurs affichées aux visiteurs en production
log_errors = On
memory_limit = 256M ; mémoire maximale par script
max_execution_time = 60 ; durée maximale d'un script, en secondes
upload_max_filesize = 20M ; taille maximale d'un fichier envoyé
post_max_size = 20M ; taille maximale d'une requête POST, au moins égale à la précédente
date.timezone = Europe/Paris
session.cookie_httponly = On ; cookie de session illisible par JavaScript
session.cookie_secure = On ; cookie envoyé seulement en HTTPS
session.cookie_samesite = Lax
sudo systemctl reload php8.4-fpm
Une limite d'envoi se règle à trois endroits, et c'est la plus petite qui gagne : client_max_body_size dans Nginx, post_max_size et upload_max_filesize dans PHP. L'erreur dit lequel bloque : 413 renvoyée par Nginx pour le premier, un formulaire qui arrive vide dans l'application pour les deux autres.
OPcache, installé et actif d'office, garde en mémoire le code PHP compilé : l'application répond plusieurs fois plus vite. Après une mise à jour du code, un rechargement de PHP-FPM vide ce cache.
Les erreurs classiques de la pile LEMP
| Symptôme | Ce que dit le journal d'erreurs de Nginx | Cause et correction |
|---|---|---|
| 502 Bad Gateway | connect() to unix:/run/php/php8.4-fpm.sock failed (2: No such file or directory) | PHP-FPM est arrêté, ou le socket ne porte pas ce nom (ls /run/php/) |
| 502 Bad Gateway | connect() … failed (13: Permission denied) | Le socket n'est pas accessible à www-data : corrige listen.owner et listen.group du pool |
| Page blanche « File not found. » | FastCGI sent in stderr: "Primary script unknown" | SCRIPT_FILENAME est faux (directive root erronée), ou le compte du pool ne peut pas lire le dossier |
Le navigateur télécharge le fichier .php | Rien | Aucun bloc location ~ \.php$ ne correspond : Nginx sert le script comme un fichier, code source compris |
| 504 Gateway Timeout | upstream timed out (110: Connection timed out) while reading response header from upstream | Script trop long : optimise-le, ou augmente ensemble fastcgi_read_timeout (60 s par défaut) et max_execution_time |
| 500 Internal Server Error | Rien côté Nginx | Erreur dans le script : lis le journal d'erreurs du pool (/var/log/php-fpm/suivi.error.log) |
Le cas du fichier téléchargé est le plus grave : le code source part chez le visiteur, avec les identifiants de base de données qu'il contiendrait. Garde les secrets hors de la racine publique (un fichier .env ou de configuration dans le dossier parent de public), pour qu'une erreur de configuration ne les expose pas (cours PHP et MySQL).
Exercices
Exercice 1 sur 3 Diagnostiquer une erreur 502 Réussi À revoir
Après une mise à jour, le site de suivi affiche « 502 Bad Gateway ». Le journal d'erreurs de Nginx contient : connect() to unix:/run/php/php8.4-fpm-suivi.sock failed (2: No such file or directory). Donne les vérifications à faire dans l'ordre, avec les commandes, et les deux causes les plus probables.
Voir la réponseMasquer la réponse de l'exercice 1
systemctl status php8.4-fpm: PHP-FPM tourne-t-il ? S'il est en échec,journalctl -u php8.4-fpm -n 30donne la raison, souvent une erreur dans un fichier de pool.sudo php-fpm8.4 -t: la configuration des pools est-elle valide ?ls -l /run/php/: quel socket existe vraiment ?
Causes probables : PHP-FPM ne démarre plus à cause d'une faute dans pool.d/suivi.conf (une erreur dans un pool empêche tout le service de démarrer), ou le pool suivi n'existe pas ou écoute sur un autre socket, par exemple si le fichier a été renommé sans l'extension .conf et n'est donc plus lu. Après correction : php-fpm8.4 -t, systemctl reload php8.4-fpm, puis curl -I sur le site.
Tu avais trouvé ?
Exercice 2 sur 3 Isoler deux applications Réussi À revoir
srv-web héberge le site de suivi et, depuis peu, un petit outil interne de prise de rendez-vous des transporteurs, /var/www/rdv. Les deux utilisent le pool www. Explique le risque, puis écris le fichier de pool de l'outil de rendez-vous et la ligne à changer dans son server block.
Voir la réponseMasquer la réponse de l'exercice 2
Le risque. Les deux applications s'exécutent sous le même compte, www-data. Une faille dans l'outil de rendez-vous (un envoi de fichier mal contrôlé, par exemple) permet de lire les fichiers du site de suivi, dont sa clé d'accès à l'API de srv-gestion, et inversement.
sudo adduser --system --group --no-create-home --home /var/www/rdv rdv
; /etc/php/8.4/fpm/pool.d/rdv.conf
[rdv]
user = rdv
group = rdv
listen = /run/php/php8.4-fpm-rdv.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = ondemand
pm.max_children = 5
pm.process_idle_timeout = 30s
php_admin_value[open_basedir] = /var/www/rdv:/tmp
php_admin_flag[display_errors] = off
Dans le server block de l'outil : fastcgi_pass unix:/run/php/php8.4-fpm-rdv.sock;. Puis php-fpm8.4 -t, systemctl reload php8.4-fpm, nginx -t, systemctl reload nginx. Avec open_basedir, un script de rdv qui tente d'ouvrir /var/www/suivi/.env reçoit un refus de PHP, en plus du refus du système si les droits du fichier sont bien posés.
Tu avais trouvé ?
Exercice 3 sur 3 Combien de processus PHP ? Réussi À revoir
srv-glpi a 4 Go de mémoire. Le système, Nginx et MariaDB en utilisent 2,5 Go sous la charge. Un processus PHP de GLPI occupe en moyenne 60 Mo. Quelle valeur maximale donnes-tu à pm.max_children ? Que se passe-t-il pour les utilisateurs si 30 requêtes PHP arrivent en même temps ?
Voir la réponseMasquer la réponse de l'exercice 3
Mémoire disponible pour PHP : 4 − 2,5 = 1,5 Go, soit 1 536 Mo. 1 536 / 60 = 25,6 : on garde 25 processus au maximum (on arrondit vers le bas, pour ne jamais dépasser la mémoire).
Avec 30 requêtes simultanées, 25 sont exécutées tout de suite ; les 5 autres attendent qu'un processus se libère. Les utilisateurs voient une page plus lente, pas une erreur, tant que l'attente reste sous les délais (fastcgi_read_timeout). Le journal de PHP-FPM signale server reached pm.max_children setting (25) : si le message revient souvent, il faut plus de mémoire, ou alléger les pages les plus lourdes.
Tu avais trouvé ?