Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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.

NavigateurGET /suivi.phpNginxprocessus www-dataHTTPSFichiers statiques/var/www/suivi/public/css/style.cssPHP-FPMpool « suivi »compte suiviFastCGI parsocket Unixpage HTMLproduitesrv-gestionAPI, TCP 8443Seuls les scripts .php passent par PHP-FPM ; Nginx lit lui-même les autres fichiers.
Nginx sert les fichiers statiques et confie les scripts PHP à PHP-FPM par FastCGI

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 :

bash
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 :

PaquetExtensionPour
php8.4-mysqlmysqli, pdo_mysqlSe connecter à MariaDB ou MySQL
php8.4-curlcurlAppeler une API, télécharger
php8.4-mbstringmbstringChaînes accentuées (UTF-8)
php8.4-intlintlDates, nombres, langues
php8.4-xmldom, simplexml, xmlreader, xmlwriterLire et écrire du XML
php8.4-gdgdImages, graphiques
php8.4-zip, php8.4-bz2zip, bz2Archives
php8.4-ldapldapAnnuaire LDAP ou Active Directory
php8.4-bcmathbcmathCalculs en grande précision
php8.4-opcacheOPcacheCache du code compilé, installé d'office avec PHP-FPM
bash
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

FichierRôle
/etc/php/8.4/fpm/php-fpm.confRéglages globaux du service (journal, PID)
/etc/php/8.4/fpm/pool.d/*.confUn fichier par pool : un groupe de processus avec son compte, son socket et ses limites
/etc/php/8.4/fpm/php.iniLes 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.logLe journal du service (démarrages, pools saturés)

Le pool par défaut, www.conf, contient ces directives (sans les commentaires) :

ini
[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
DirectiveRôle
user, groupLe compte sous lequel les scripts PHP s'exécutent
listenLe socket (ou 127.0.0.1:9000 pour un port TCP) sur lequel le pool attend Nginx
listen.owner, listen.groupLe propriétaire du socket : Nginx (www-data) doit pouvoir y écrire
pmLa 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_childrenLe nombre maximal de scripts exécutés en même temps
pm.start_servers, pm.min_spare_servers, pm.max_spare_serversLe 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.

bash
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 :

nginx
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 :

bash
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 :

bash
sudo adduser --system --group --no-create-home --home /var/www/suivi suivi

Puis /etc/php/8.4/fpm/pool.d/suivi.conf :

ini
[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
bash
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églageEffet
user = suiviLes scripts du site ne peuvent écrire que là où le compte suivi en a le droit
listen.owner = www-dataNginx, seul, peut parler au pool
pm = ondemandAucun processus quand le site est inactif, jusqu'à 10 sous la charge
pm.max_requests = 500Chaque 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 :

ini
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
bash
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ômeCe que dit le journal d'erreurs de NginxCause et correction
502 Bad Gatewayconnect() 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 Gatewayconnect() … 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 .phpRienAucun bloc location ~ \.php$ ne correspond : Nginx sert le script comme un fichier, code source compris
504 Gateway Timeoutupstream timed out (110: Connection timed out) while reading response header from upstreamScript trop long : optimise-le, ou augmente ensemble fastcgi_read_timeout (60 s par défaut) et max_execution_time
500 Internal Server ErrorRien côté NginxErreur 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
  1. systemctl status php8.4-fpm : PHP-FPM tourne-t-il ? S'il est en échec, journalctl -u php8.4-fpm -n 30 donne la raison, souvent une erreur dans un fichier de pool.
  2. sudo php-fpm8.4 -t : la configuration des pools est-elle valide ?
  3. 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.

bash
sudo adduser --system --group --no-create-home --home /var/www/rdv rdv
ini
; /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é ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : MariaDB sur Debian 13 : installer, sécuriser et créer les comptesRevoir le sommaire du cours