Nginx da EGPHP
La porta d'ingresso, costruita come si deve.
Nginx decide cosa arriva al tuo sito: quale server e quale pool PHP rispondono, cosa esce dalla cache e cosa viene respinto già sulla porta. Ecco come lo configura il nostro team, con la configurazione a dimostrarlo.
NGINX
PHP
Redis
Una richiesta, dalla porta al tuo codice
La maggior parte delle richieste non ha alcun bisogno di PHP. Ogni tappa qui sotto può rispondere da sola, e solo ciò che resta prosegue verso la successiva.
-
La connessione
Cifrata con TLS 1.3, e un'unica connessione porta tutti i file della pagina su HTTP/2 o HTTP/3.
-
Limiti
Troppe richieste da un solo indirizzo, troppe connessioni aperte o un corpo più grande di quanto il sito accetti: la risposta arriva qui.
429 o 413: si ferma qui -
Percorsi vietati
File nascosti, backup e qualsiasi script dentro una cartella di upload vengono rifiutati prima che PHP ne sappia nulla.
403 o 404: si ferma qui -
File statici
Immagini, fogli di stile e script partono direttamente dal disco, con lunghe durate di cache.
Servito: si ferma qui -
Cache di pagina
Un visitatore non autenticato riceve una pagina finita dalla memoria.
Trovata in cache: si ferma qui -
Il bilanciatore
Sceglie il server e il pool PHP che hanno posto per la richiesta.
-
Il pool dell'account
PHP gira con l'utente dell'account, dentro le sue cartelle, con il suo php.ini.
Bilanciamento del carico tra server
Quando un server non basta, Nginx si mette davanti a più server e distribuisce il traffico. Il metodo decide chi prende la richiesta successiva.
Il predefinito. Ogni server prende a turno la richiesta successiva: la scelta giusta quando i server sono simili e le richieste costano più o meno lo stesso.
La richiesta successiva va al server con meno connessioni aperte: la scelta giusta quando alcune richieste durano a lungo, come report, esportazioni e upload.
L'indirizzo del visitatore sceglie il server, così lo stesso visitatore finisce sempre sullo stesso. Serve alle applicazioni che tengono le sessioni sul disco locale; meglio ancora, spostiamo le sessioni su Redis e il vincolo sparisce.
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
- Controlli di salute sul traffico reale: dopo tre tentativi falliti in trenta secondi, un server viene escluso per trenta secondi, poi riprovato.
- backup
- Un server di riserva che riceve traffico solo quando tutti gli altri non sono disponibili.
- proxy_next_upstream
- Una richiesta che incontra un errore o un timeout viene ritentata sul server successivo. Una richiesta che modifica dati non viene inviata due volte, così un ordine non viene mai registrato due volte.
- keepalive
- Le connessioni verso i server restano aperte tra una richiesta e l'altra, invece di essere riaperte ogni volta.
- weight
- A un server più grande si può assegnare una quota maggiore delle richieste.
- down
- Un server viene tolto per manutenzione con un reload, mentre gli altri reggono il traffico.
Bilanciamento del carico tra pool PHP
La stessa idea dentro un singolo server. Un sito può avere alle spalle più di un pool PHP-FPM, così un pool può riavviarsi mentre l'altro risponde, e il lavoro lento non sottrae mai i worker che servono al negozio.
~ \.php$^~ /admin/
shop-a.sock
Negozio, pool A
Risponde
Risponde, e prende anche le richieste di B
shop-b.sock
Negozio, pool B
Risponde
Si riavvia con una nuova impostazione: nessun visitatore deve aspettare
shop-admin.sock
Amministrazione e report
Worker propri, separati dal negozio
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;
}
}
-
Due pool, un sito
Entrambi i pool eseguono lo stesso codice con lo stesso account. Una nuova impostazione PHP arriva a un pool, poi all'altro, e i visitatori continuano a ricevere risposta per tutto il tempo.
-
Subentro automatico
fastcgi_next_upstreamsposta una richiesta sull'altro pool quando il suo socket rifiuta la connessione o va in timeout. Una richiesta che modifica dati non viene inviata due volte. -
Una corsia per il lavoro lento
L'area di amministrazione, le esportazioni e i report hanno un pool tutto loro, così un report di un'ora non può togliere i worker di cui un cliente ha bisogno per completare l'acquisto.
- Come dimensioniamo un pool PHP-FPM PHP da EGPHP
Le regole sulla porta
La protezione comincia prima che PHP esegua una sola riga. Queste regole entrano in ogni blocco server che scriviamo, adattate al sito.
# 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;}
-
Limiti di frequenza e di connessione
Ogni indirizzo ha un ritmo costante con margine per un breve picco; oltre quello, Nginx risponde 429 e PHP non viene mai svegliato. La pagina di login ha un limite tutto suo, molto più stretto, che toglie il fiato a chi prova a indovinare le password.
-
Dimensione del corpo e timeout
Un upload più grande di quanto il sito accetti viene rifiutato subito, e un client che invia la richiesta molto lentamente viene disconnesso invece di tenere aperta una connessione.
-
File nascosti e avanzi
Tutto ciò il cui nome inizia con un punto, come
.gito.env, e le copie di backup dei file non vengono mai serviti. L'unica eccezione è la cartella usata per le verifiche dei certificati. -
Gli upload non eseguono mai PHP
Una cartella che accetta file dai visitatori li serve come file. Uno script messo lì viene rifiutato, qualunque sia il suo nome.
-
Solo un file PHP che esiste davvero
try_files $uri =404passa una richiesta a PHP solo se quel file esatto si trova sul disco. Questo chiude la classica trappola disegnata qui sotto. -
Header e TLS
HSTS tiene i browser su HTTPS, gli altri header impediscono che il tipo di una pagina venga indovinato o che la pagina venga inserita in un frame da un altro sito, e il server non dichiara la sua versione. Solo TLS 1.2 e 1.3.
Come proteggiamo il server stesso
La trappola che apre una riga mancante
/uploads/avatar.jpgUn file vero: un'immagine con codice PHP nascosto dentro
/shell.phpNon esiste
Su una configurazione vecchia o trascurata, PHP risale il percorso all'indietro, trova l'immagine ed esegue il codice che contiene.
Un 404, e non viene eseguito nulla. cgi.fix_pathinfo=0 in PHP e security.limit_extensions nel pool sono la seconda e la terza serratura sulla stessa porta.
Ogni account è una stanza murata
Su un server condiviso da più clienti, un sito non deve mai poterne leggere un altro. Costruiamo quel muro a strati, così nessuna singola impostazione lo regge da sola.
shopL'account- Utente Linux
- shop
- Pool PHP
- /run/php/shop.sock
- Cartelle consentite
- /home/shop
- Impostazioni
- pool.d/shop.conf
clinicL'account- Utente Linux
- clinic
- Pool PHP
- /run/php/clinic.sock
- Cartelle consentite
- /home/clinic
- Impostazioni
- pool.d/clinic.conf
blogL'account- Utente Linux
- blog
- Pool PHP
- /run/php/blog.sock
- Cartelle consentite
- /home/blog
- Impostazioni
- pool.d/blog.conf
file_get_contents('/home/clinic/public_html/config.php')
Permission deniedopen_basedir restriction in effect
-
Un utente Linux proprio
Ogni sito gira con il proprio utente, e ogni cartella home è chiusa agli altri account. È il muro che regge anche se PHP resta fuori dal quadro.
-
Un pool PHP-FPM proprio
Un pool per account, raggiungibile solo attraverso il suo socket. Un sito sotto carico o guasto consuma i propri worker, non quelli dei vicini.
-
open_basedireinclude_pathPHP può aprire file solo dentro le cartelle dell'account, e cerca i file inclusi soltanto lì. Uno script che chiede un percorso al di fuori viene respinto prima ancora di toccare il disco.
-
File temporanei e sessioni propri
Gli upload in corso e i file di sessione restano dentro l'account, non in una cartella
/tmpcondivisa dove qualunque sito potrebbe leggere le sessioni di login di un altro. -
Nessuna funzione che al sito non serve
open_basedirnon è una sandbox: non ferma un programma avviato da PHP. Per questo le funzioni che eseguono comandi di shell vengono disattivate account per account, a meno che il sito non ne abbia davvero bisogno.
[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
Un php.ini per ogni account
Ogni account ha le sue impostazioni, scritte nel suo pool. Alcune le fissiamo noi, altre il sito può cambiarle, e la differenza sta in una parola.
php_admin_value: fisso
Impostato per l'account. Né ini_set() né un file .user.ini possono cambiarlo.
memory_limit = 256M
max_execution_time = 60
open_basedir = /home/shop/…
disable_functions = exec,…
php_value: un valore di partenza
Il nostro valore predefinito per l'account. Il sito può alzarlo o abbassarlo dove PHP lo consente.
upload_max_filesize = 32M
post_max_size = 32M
max_input_vars = 3000
.user.ini: quello del sito
Un file nella cartella del sito per le impostazioni che gli è permesso cambiare. Per impostazione predefinita PHP lo rilegge ogni cinque minuti.
upload_max_filesize = 64M
post_max_size = 64M
display_errors = Off
Un esempio: i valori si fissano sito per sito
Limiti che devono andare d'accordo
upload_max_filesizePHP≤
post_max_sizePHP≤
client_max_body_sizeNginx
Se Nginx consente meno, l'upload viene rifiutato con un 413 prima che PHP lo veda; se è PHP a consentire meno, il file arriva e viene buttato via.
max_execution_timePHP<
request_terminate_timeoutPHP-FPM≤
fastcgi_read_timeoutNginx
PHP dovrebbe fermarsi per primo, con un errore che può registrare; il pool ferma ciò che PHP non è riuscito a fermare; e Nginx aspetta abbastanza da non mostrare un 504 mentre PHP sta ancora lavorando.
La cache, a strati
Ogni strato conserva una cosa diversa, e una richiesta scende solo fin dove serve. Passa da un visitatore all'altro per vedere dove si ferma ciascuno.
-
Il browserFogli di stile, script e immagini con una versione nel nome, conservati per un anno.Ha già i fileHa già i file
-
Cache di pagina NginxPagine finite, conservate da pochi secondi a qualche minuto.Trovata: la pagina parte da quiSaltata: un cookie di login o di carrello rende la pagina personale
-
OPcachePHP compilato una volta sola e tenuto in memoria.Non raggiuntoIl codice gira senza essere riletto né ricompilato
-
RedisRisposte ripetute del database, e sessioni.Non raggiuntoLa maggior parte delle risposte arriva dalla memoria
-
Il databaseLa fonte di verità.Non raggiuntoInterrogato solo per ciò che Redis non aveva
# 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 chiave
- Schema, metodo, host e indirizzo completo, così due pagine diverse non condividono mai una voce.
- L'esclusione
- Un login o un carrello salta la cache in entrambe le direzioni: la pagina non viene né servita dalla cache né salvata al suo interno. I moduli inviati con
POSTnon finiscono mai in cache. - Copia vecchia durante l'aggiornamento
- Quando una voce scade, una sola richiesta la rinnova in background mentre tutti gli altri ricevono ancora la copia precedente. Se PHP fallisce, viene servita l'ultima pagina buona.
- Una regola che molti trascurano
- Un
add_headerdentro un blocco location sostituisce tutti gli header impostati più in alto, perciò teniamo gli header di sicurezza in un unico snippet e lo includiamo ovunque si aggiungano header.
Velocità, e il resto di una buona configurazione
Le impostazioni che rendono più veloce ogni sito sul server, e le abitudini che rendono sicura ogni modifica.
$ 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 nuova configurazione viene prima verificata; se il test fallisce, non cambia nulla. Al reload, i nuovi worker partono con essa mentre i vecchi finiscono le richieste che hanno in mano, così nessun visitatore viene tagliato fuori.
log_format timed '$remote_addr "$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'cache=$upstream_cache_status';
Ogni riga di log registra il tempo totale, il tempo impiegato da PHP e cosa ha fatto la cache, così una pagina lenta si attribuisce a Nginx, a PHP o alla rete invece di andare a intuito.
- http2 on; listen 443 quic;
- Un'unica connessione porta l'intera pagina; HTTP/3 si aggiunge dove la build e la rete lo consentono.
- gzip · brotli
- Il testo viene compresso prima di partire, con Brotli dove il modulo è incluso nella build. Immagini e video non vengono compressi due volte.
- sendfile · tcp_nopush
- È il kernel stesso a inviare i file dal disco alla rete, in pacchetti pieni.
- open_file_cache
- Nginx ricorda quali file esistono e dove si trovano, invece di chiederlo al disco a ogni richiesta.
- worker_processes auto
- Un worker per core del processore, ognuno con molte connessioni in un unico ciclo di eventi.
- Cache-Control: immutable
- I file con una versione nel nome restano in cache per un anno e non vengono mai più ricontrollati.
Cosa c'è dietro la porta d'ingresso
Raggruppato per compito di ogni parte, tutto deciso nella configurazione di Nginx.
Connessioni
- Connessioni cifrateTLS 1.3 cifra la connessione e HTTP/2 porta tutti i file di una pagina su un'unica connessione.
- Limiti di frequenza e dimensioneTroppe richieste, troppe connessioni aperte o un corpo troppo grande ricevono risposta alla porta.
Consegna
- File staticiImmagini, stili e script partono direttamente dal disco, con lunghi tempi di cache.
- Cache di paginaUn visitatore non autenticato riceve una pagina finita dalla memoria.
- Bilanciamento del caricoLe richieste vanno al server e al pool PHP che hanno posto per loro.
Protezione
- Percorsi vietatiFile nascosti, backup e script nelle cartelle di upload vengono rifiutati prima che PHP ne sappia nulla.
- Header di sicurezzaTenuti in un solo frammento e inclusi ovunque si aggiungano header.
- Account isolatiOgni account esegue PHP come proprio utente, dentro le proprie cartelle.
Cambiare la configurazione senza perdere un visitatore
Nginx applica una nuova configurazione sul posto. Ecco come la sua documentazione descrive l'ordine degli eventi.
- Testare il file
Il comando nginx -t legge tutta la configurazione e si ferma al primo errore, prima che qualcosa ci giri sopra.
- Inviare il segnale di ricarica
Il processo master riceve il segnale e controlla la nuova configurazione. Se il controllo fallisce, torna indietro e la vecchia configurazione continua a servire.
- Partono nuovi worker
Se il controllo riesce, il master avvia nuovi processi worker che usano la nuova configurazione.
- I vecchi worker finiscono
I vecchi worker smettono di accettare nuove connessioni e finiscono le richieste che hanno già.
- I vecchi worker escono
Quando l'ultimo visitatore è servito si spengono e restano solo worker con la nuova configurazione.

Portaci il sito. Noi costruiremo la sua porta d'ingresso.
Dicci dove gira oggi e di cosa ha bisogno. Prima leggiamo la configurazione attuale, poi proponiamo quella per Nginx e PHP, e prepariamo il preventivo dopo quella conversazione.