Nginx chez EGPHP
La porte d'entrée, construite dans les règles.
Nginx décide de ce qui atteint votre site : quel serveur et quel pool PHP répondent, ce qui vient du cache et ce qui est refoulé dès la porte. Voici comment notre équipe le met en place, configuration à l'appui.
NGINX
PHP
Redis
Une requête, de la porte jusqu'à votre code
La plupart des requêtes n'ont jamais besoin de PHP. Chaque étape ci-dessous peut répondre seule, et seul ce qui reste passe à la suivante.
-
La connexion
Chiffrée en TLS 1.3, et une seule connexion transporte tous les fichiers de la page en HTTP/2 ou HTTP/3.
-
Limites
Trop de requêtes depuis une même adresse, trop de connexions ouvertes ou un corps de requête plus gros que ce que le site accepte : la réponse part d'ici.
429 ou 413 : arrêt ici -
Chemins interdits
Les fichiers cachés, les sauvegardes et tout script placé dans un dossier d'upload sont refusés avant que PHP n'en entende parler.
403 ou 404 : arrêt ici -
Fichiers statiques
Images, feuilles de style et scripts partent directement du disque, avec de longues durées de cache.
Servi : arrêt ici -
Cache de pages
Un visiteur non connecté reçoit une page terminée depuis la mémoire.
Succès du cache : arrêt ici -
Le répartiteur
Choisit le serveur et le pool PHP qui ont de la place pour la requête.
-
Le pool propre au compte
PHP tourne sous l'utilisateur du compte, dans ses propres dossiers, avec son propre php.ini.
Répartition de charge entre serveurs
Quand un serveur ne suffit plus, Nginx se place devant plusieurs et répartit le trafic entre eux. La méthode décide qui prend la requête suivante.
Le choix par défaut. Chaque serveur prend la requête suivante à tour de rôle : adapté quand les serveurs se valent et que les requêtes coûtent à peu près autant.
La requête suivante va au serveur qui a le moins de connexions ouvertes : adapté quand certaines requêtes sont longues, comme les rapports, les exports et les uploads.
L'adresse du visiteur choisit le serveur, si bien que le même visiteur retombe toujours sur le même. Pour les applications qui gardent leurs sessions sur le disque local ; mieux encore, nous déplaçons les sessions vers Redis et le besoin disparaît.
upstream app_servers {
# round robin: the default, no line needed least_conn; ip_hash;
server 10.0.0.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.13:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.14:8080 backup; # no backup server with ip_hash
keepalive 32;
}
server {
server_name example.com;
location / {
proxy_pass http://app_servers;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_next_upstream error timeout http_502 http_503;
}
}
- max_fails · fail_timeout
- Contrôles de santé sur le trafic réel : après trois échecs en trente secondes, un serveur est écarté pendant trente secondes, puis remis à l'essai.
- backup
- Un serveur de secours qui ne reçoit du trafic que lorsque tous les autres serveurs sont indisponibles.
- proxy_next_upstream
- Une requête qui rencontre une erreur ou un délai dépassé est retentée sur le serveur suivant. Une requête qui modifie des données n'est pas envoyée deux fois : une commande n'est donc jamais passée en double.
- keepalive
- Les connexions vers les serveurs restent ouvertes entre les requêtes au lieu d'être rouvertes pour chacune.
- weight
- Un serveur plus puissant peut recevoir une plus grande part des requêtes.
- down
- Un serveur est retiré pour maintenance par un simple rechargement, pendant que les autres assurent le trafic.
Répartition de charge entre pools PHP
La même idée à l'intérieur d'un seul serveur. Un site peut avoir plusieurs pools PHP-FPM derrière lui : un pool peut redémarrer pendant que l'autre répond, et le travail lent ne prend jamais les workers dont la boutique a besoin.
~ \.php$^~ /admin/
shop-a.sock
Boutique, pool A
Répond
Répond et prend les requêtes de B
shop-b.sock
Boutique, pool B
Répond
Redémarre avec un nouveau réglage : aucun visiteur ne l'attend
shop-admin.sock
Administration et rapports
Ses propres workers, à l'écart de la boutique
upstream php_shop {
server unix:/run/php/shop-a.sock max_fails=2 fail_timeout=10s;
server unix:/run/php/shop-b.sock max_fails=2 fail_timeout=10s;
keepalive 8;
}
# server {}
location ~ \.php$ {
try_files $uri =404;
include fastcgi.conf;
fastcgi_pass php_shop;
fastcgi_keep_conn on;
fastcgi_next_upstream error timeout;
}
location ^~ /admin/ {
location ~ \.php$ {
try_files $uri =404;
include fastcgi.conf;
fastcgi_pass unix:/run/php/shop-admin.sock;
}
}
-
Deux pools, un site
Les deux pools exécutent le même code sous le même compte. Un nouveau réglage PHP atteint un pool, puis l'autre, et les visiteurs continuent d'être servis tout du long.
-
Basculement
fastcgi_next_upstreamrenvoie une requête vers l'autre pool quand le socket du premier refuse la connexion ou dépasse son délai. Une requête qui modifie des données n'est pas envoyée deux fois. -
Une voie pour le travail lent
L'espace d'administration, les exports et les rapports ont leur propre pool : un rapport d'une heure ne peut pas accaparer les workers dont un client a besoin pour régler sa commande.
- Comment nous dimensionnons un pool PHP-FPM Le PHP chez EGPHP
Les règles à la porte
La protection commence avant que PHP n'exécute une seule ligne. Ces règles entrent dans chaque bloc server que nous écrivons, ajustées au site.
# http {}
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=conns:10m;server {
listen 443 ssl;
http2 on;
server_name example.com;
limit_req zone=perip burst=40 nodelay;
limit_conn conns 30;
limit_req_status 429;
location = /wp-login.php {
limit_req zone=login burst=5 nodelay;
include fastcgi.conf;
fastcgi_pass php_shop;
} client_max_body_size 64m;
client_header_timeout 15s;
client_body_timeout 15s; location ~ /\.(?!well-known/) { deny all; }
location ~* \.(?:bak|old|orig|sql|swp)$ { deny all; } location ~* ^/(?:wp-content/uploads|uploads|files)/.*\.php$ {
deny all;
} location ~ \.php$ {
try_files $uri =404;
include fastcgi.conf;
fastcgi_pass php_shop;
} ssl_protocols TLSv1.2 TLSv1.3;
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;}
-
Limites de débit et de connexions
Chaque adresse a droit à un débit régulier, avec une marge pour une courte rafale ; au-delà, Nginx répond 429 et PHP n'est jamais réveillé. La page de connexion a sa propre limite, bien plus stricte, qui coupe l'herbe sous le pied à qui tente de deviner les mots de passe.
-
Taille du corps et délais
Un upload plus gros que ce que le site accepte est refusé d'emblée, et un client qui envoie sa requête très lentement est déconnecté au lieu de garder une connexion ouverte.
-
Fichiers cachés et restes
Tout ce dont le nom commence par un point, comme
.gitou.env, ainsi que les copies de sauvegarde de fichiers, n'est jamais servi. La seule exception est le dossier qu'utilisent les vérifications de certificat. -
Les uploads n'exécutent jamais PHP
Un dossier qui reçoit des fichiers de visiteurs les sert en tant que fichiers. Un script déposé là est refusé, quel que soit son nom.
-
Seulement un fichier PHP qui existe
try_files $uri =404ne transmet une requête à PHP que si ce fichier précis se trouve sur le disque. Cela referme le piège classique illustré ci-dessous. -
En-têtes et TLS
HSTS maintient les navigateurs en HTTPS, les autres en-têtes empêchent le navigateur de deviner le type d'une page et un autre site de l'afficher dans un cadre, et le serveur n'annonce pas sa version. TLS 1.2 et 1.3 uniquement.
Comment nous sécurisons le serveur lui-même
Le piège qu'ouvre une seule ligne manquante
/uploads/avatar.jpgUn vrai fichier : une image avec du code PHP caché à l'intérieur
/shell.phpN'existe pas
Sur une configuration ancienne ou négligée, PHP remonte le chemin, trouve l'image et exécute le code qu'elle contient.
Une 404, et rien ne s'exécute. cgi.fix_pathinfo=0 côté PHP et security.limit_extensions dans le pool sont les deuxième et troisième verrous de la même porte.
Chaque compte est une pièce entre quatre murs
Sur un serveur partagé par plusieurs clients, un site ne doit jamais pouvoir lire un autre site. Nous construisons ce mur par couches, pour qu'aucun réglage ne le porte à lui seul.
shopCompte- Utilisateur Linux
- shop
- Pool PHP
- /run/php/shop.sock
- Dossiers autorisés
- /home/shop
- Réglages
- pool.d/shop.conf
clinicCompte- Utilisateur Linux
- clinic
- Pool PHP
- /run/php/clinic.sock
- Dossiers autorisés
- /home/clinic
- Réglages
- pool.d/clinic.conf
blogCompte- Utilisateur Linux
- blog
- Pool PHP
- /run/php/blog.sock
- Dossiers autorisés
- /home/blog
- Réglages
- pool.d/blog.conf
file_get_contents('/home/clinic/public_html/config.php')
Permission deniedopen_basedir restriction in effect
-
Son propre utilisateur Linux
Chaque site tourne sous son propre utilisateur, et chaque dossier personnel est fermé aux autres comptes. C'est le mur qui tient même si l'on fait abstraction de PHP.
-
Son propre pool PHP-FPM
Un pool par compte, joignable uniquement par son propre socket. Un site surchargé ou en panne épuise ses propres workers, pas ceux de ses voisins.
-
open_basediretinclude_pathPHP ne peut ouvrir des fichiers que dans les dossiers du compte, et n'y cherche que les fichiers inclus. Un script qui demande un chemin en dehors est refusé avant que le disque ne soit touché.
-
Ses propres fichiers temporaires et sessions
Les uploads en cours et les fichiers de session restent dans le compte, pas dans un
/tmppartagé où n'importe quel site pourrait lire les sessions connectées d'un autre. -
Aucune fonction dont le site n'a pas besoin
open_basedirn'est pas un bac à sable : il n'arrête pas un programme lancé depuis PHP. Les fonctions qui exécutent des commandes shell sont donc désactivées compte par compte, sauf si le site en a réellement besoin.
[shop]
user = shop
group = shop
listen = /run/php/shop.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = ondemand
pm.max_children = 12
pm.process_idle_timeout = 10s
php_admin_value[open_basedir] = /home/shop/public_html:/home/shop/lib:/home/shop/tmp
php_admin_value[include_path] = .:/home/shop/lib
php_admin_value[upload_tmp_dir] = /home/shop/tmp
php_admin_value[sys_temp_dir] = /home/shop/tmp
php_admin_value[session.save_path] = /home/shop/tmp/sessions
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen
php_admin_flag[allow_url_include] = off
php.ini, par compte
Chaque compte a ses propres réglages, inscrits dans son propre pool. Certains sont fixés par nous, d'autres peuvent être modifiés par le site, et la différence tient en un mot.
php_admin_value : fixé
Défini pour le compte. Ni ini_set() ni un fichier .user.ini ne peuvent le modifier.
memory_limit = 256M
max_execution_time = 60
open_basedir = /home/shop/…
disable_functions = exec,…
php_value : une valeur de départ
Notre valeur par défaut pour le compte. Le site peut l'augmenter ou la réduire là où PHP le permet.
upload_max_filesize = 32M
post_max_size = 32M
max_input_vars = 3000
.user.ini : propre au site
Un fichier dans le dossier du site pour les réglages qu'il a le droit de modifier. Par défaut, PHP le relit toutes les cinq minutes.
upload_max_filesize = 64M
post_max_size = 64M
display_errors = Off
Un exemple : les valeurs sont fixées site par site
Des limites qui doivent concorder
upload_max_filesizePHP≤
post_max_sizePHP≤
client_max_body_sizeNginx
Si Nginx autorise moins, l'upload est refusé avec une 413 avant que PHP ne le voie ; si c'est PHP qui autorise moins, le fichier arrive et il est jeté.
max_execution_timePHP<
request_terminate_timeoutPHP-FPM≤
fastcgi_read_timeoutNginx
PHP doit s'arrêter lui-même en premier, avec une erreur qu'il peut journaliser ; le pool arrête ce que PHP n'a pas pu arrêter ; et Nginx attend assez longtemps pour ne pas afficher une 504 pendant que PHP travaille encore.
Le cache, par couches
Chaque couche garde quelque chose de différent, et une requête ne descend qu'aussi loin que nécessaire. Passez d'un visiteur à l'autre pour voir où chacun s'arrête.
-
NavigateurFeuilles de style, scripts et images avec une version dans leur nom, gardés un an.A déjà les fichiersA déjà les fichiers
-
Cache de pages NginxDes pages terminées, gardées de quelques secondes à quelques minutes.Succès : la page est servie iciIgnoré : un cookie de connexion ou de panier rend la page personnelle
-
OPcachePHP compilé une fois et gardé en mémoire.Pas atteintLe code s'exécute sans être relu ni recompilé
-
RedisLes réponses répétées de la base de données, et les sessions.Pas atteintLa plupart des réponses viennent de la mémoire
-
Base de donnéesLa source de vérité.Pas atteintInterrogée seulement pour ce que Redis n'avait pas
# http {}
fastcgi_cache_path /var/cache/nginx/shop levels=1:2
keys_zone=shop:50m inactive=60m max_size=2g;
map $http_cookie $skip_cache {
default 0;
~wordpress_logged_in_ 1;
~woocommerce_items_in_cart 1;
}
# location ~ \.php$ {}
fastcgi_cache shop;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 301 10s;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_use_stale updating error timeout http_500 http_503;
fastcgi_cache_background_update on;
fastcgi_cache_lock on;
- La clé
- Schéma, méthode, hôte et adresse complète, pour que deux pages différentes ne partagent jamais une entrée.
- Le contournement
- Une session connectée ou un panier contourne le cache dans les deux sens : la page n'en est pas servie et n'y est pas stockée. Les formulaires envoyés en
POSTne sont jamais mis en cache. - Servir l'ancienne copie pendant la mise à jour
- Quand une entrée expire, une seule requête la rafraîchit en arrière-plan pendant que tous les autres reçoivent encore la copie précédente. Si PHP échoue, la dernière page valide est servie.
- Une règle souvent oubliée
- Un
add_headerdans un bloc location remplace tous les en-têtes définis plus haut : nous gardons donc les en-têtes de sécurité dans un seul fragment, inclus partout où des en-têtes sont ajoutés.
La vitesse, et le reste d'une bonne configuration
Les réglages qui rendent chaque site du serveur plus rapide, et les habitudes qui rendent un changement sûr.
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo systemctl reload nginx
La nouvelle configuration est d'abord testée ; si le test échoue, rien ne change. Au rechargement, de nouveaux workers démarrent avec elle pendant que les anciens terminent les requêtes en cours, et aucun visiteur n'est coupé.
log_format timed '$remote_addr "$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'cache=$upstream_cache_status';
Chaque ligne de journal enregistre la durée totale, le temps pris par PHP et ce qu'a fait le cache : une page lente est ainsi imputée à Nginx, à PHP ou au réseau au lieu d'être devinée.
- http2 on; listen 443 quic;
- Une seule connexion transporte toute la page ; HTTP/3 s'y ajoute là où la compilation de Nginx et le réseau le permettent.
- gzip · brotli
- Le texte est compressé avant de partir, avec Brotli là où le module est compilé. Les images et la vidéo ne sont pas compressées deux fois.
- sendfile · tcp_nopush
- Le noyau envoie lui-même les fichiers du disque vers le réseau, en paquets pleins.
- open_file_cache
- Nginx retient quels fichiers existent et où ils se trouvent, au lieu d'interroger le disque à chaque requête.
- worker_processes auto
- Un worker par cœur de processeur, chacun gérant de nombreuses connexions dans une seule boucle d'événements.
- Cache-Control: immutable
- Les fichiers avec une version dans leur nom sont mis en cache pour un an et ne sont plus jamais revérifiés.
Ce qui se trouve derrière la porte d'entrée
Regroupé selon le rôle de chaque élément, le tout décidé dans la configuration Nginx.
Connexions
- Connexions chiffréesTLS 1.3 chiffre la connexion et HTTP/2 transporte tous les fichiers d'une page sur une seule connexion.
- Limites de débit et de tailleTrop de requêtes, trop de connexions ouvertes ou un corps trop volumineux sont traités dès la porte.
Livraison
- Fichiers statiquesImages, styles et scripts partent directement du disque, avec de longues durées de cache.
- Cache de pagesUn visiteur non connecté reçoit une page terminée depuis la mémoire.
- Répartition de chargeLes requêtes vont au serveur et au pool PHP qui ont de la place pour elles.
Protection
- Chemins interditsLes fichiers cachés, sauvegardes et scripts dans les dossiers d'envoi sont refusés avant que PHP n'en entende parler.
- En-têtes de sécuritéGardés dans un seul extrait, inclus partout où des en-têtes sont ajoutés.
- Comptes cloisonnésChaque compte exécute PHP sous son propre utilisateur, dans ses propres dossiers.
Changer la configuration sans lâcher un visiteur
Nginx applique une nouvelle configuration sur place. Voici l'ordre des événements tel que le décrit sa documentation.
- Tester le fichier
La commande nginx -t lit toute la configuration et s'arrête à la première erreur, avant que quoi que ce soit ne tourne dessus.
- Envoyer le signal de rechargement
Le processus maître reçoit le signal et vérifie la nouvelle configuration. Si la vérification échoue, il revient en arrière et l'ancienne configuration continue de servir.
- De nouveaux workers démarrent
Si la vérification réussit, le maître démarre de nouveaux processus workers qui utilisent la nouvelle configuration.
- Les anciens workers terminent
Les anciens workers n'acceptent plus de nouvelles connexions et terminent les requêtes qu'ils tiennent déjà.
- Les anciens workers s'arrêtent
Quand le dernier visiteur est servi, ils s'arrêtent et il ne reste que des workers sur la nouvelle configuration.

Confiez-nous le site. Nous construirons sa porte d'entrée.
Dites-nous où il tourne aujourd'hui et ce dont il a besoin. Nous examinons d'abord la configuration actuelle, puis proposons la configuration Nginx et PHP, et chiffrons le travail après cet échange.