Errori PHP critici su VPS gestiti senza supporto tecnico: guida operativa per il ripristino

Errori PHP critici su VPS gestiti senza supporto tecnico: guida operativa per il ripristino

Il 14 aprile 2025, un martedì mattina alle 9:15, mi ha chiamato il titolare di una PMI toscana che vende arredamento contract tramite un e-commerce Laravel 10 su un Hetzner CPX21 - 3 vCPU AMD, 4 GB di RAM, 80 GB NVMe - con Debian 12, Nginx 1.22, PHP-FPM 8.2 e MySQL 8.0. Il sito mostrava una schermata bianca: nessun messaggio di errore, nessuna pagina 500, solo un body HTML completamente vuoto. Il pannello Hetzner Cloud mostrava il VPS online con risorse normali (CPU al 5%, RAM al 60%), Nginx rispondeva con HTTP 200 ma il body era vuoto, e MySQL funzionava perfettamente. Il log di Nginx non mostrava errori. Il log di PHP-FPM non mostrava errori. Il laravel.log non mostrava errori. Era come se PHP avesse semplicemente smesso di produrre output.

Quel tipo di schermata bianca - HTTP 200, nessun log, nessun errore visibile - è il sintomo più insidioso che un sysadmin possa incontrare su un VPS con PHP, perché non lascia tracce nei posti dove normalmente si guarda. In venti minuti di diagnosi ho identificato il problema: un segfault in OPcache che uccideva il processo PHP-FPM prima che potesse scrivere qualsiasi cosa nel log. L'aggiornamento automatico di sicurezza applicato la notte precedente aveva portato PHP da 8.2.18 a 8.2.21, e la nuova versione di OPcache aveva un bug noto nel file cache che causava crash intermittenti sotto carico. In questo articolo ti racconto la diagnosi completa e il protocollo che uso per ogni emergenza PHP su VPS, perché la schermata bianca è solo il sintomo più comune di una classe di problemi che ha almeno cinque cause radice diverse.

Stai cercando un Consulente Informatico esperto per risolvere emergenze PHP sulla tua infrastruttura? Nel mio profilo professionale trovi l'esperienza concreta su stack LEMP, PHP-FPM e ottimizzazione di applicazioni Laravel e Symfony. Contattami per una consulenza diretta.

Come si diagnostica una schermata bianca PHP quando i log non mostrano nulla?

La schermata bianca (WSOD - White Screen of Death) con HTTP 200 e log vuoti è quasi sempre un crash del processo PHP a livello di sistema operativo, non un errore PHP gestito. Quando PHP incontra un errore gestito (syntax error, memory exhausted, exception non catturata), produce un messaggio nel log e restituisce un HTTP 500. Quando il processo crasha - segfault, signal 11, OOM killer - muore prima di poter scrivere qualsiasi cosa, e Nginx riceve una risposta vuota dal socket FPM che viene servita come 200 con body vuoto.

La diagnosi parte dal kernel, non da PHP:

# 1. Cercare segfault nel kernel log (IL PRIMO POSTO DOVE GUARDARE)
dmesg -T | grep -i "segfault\|killed\|oom\|signal 11"

# 2. Cercare nel journal di systemd
journalctl -u php8.2-fpm --since "1 hour ago" --no-pager | tail -30

# 3. Se dmesg non mostra nulla, attivare il core dump di PHP-FPM
# In /etc/php/8.2/fpm/php-fpm.conf:
# rlimit_core = unlimited
# In /etc/sysctl.conf:
# kernel.core_pattern = /tmp/core-%e-%p-%t
# Poi: sysctl -p && systemctl restart php8.2-fpm

Nel caso toscano, dmesg ha rivelato immediatamente il problema:

[Tue Apr 14 08:47:12 2025] php-fpm8.2[14523]: segfault at 7f3a2c001020 ip 00007f3a4b2e8a31 sp 00007fff5c8e3b90 error 4 in opcache.so[7f3a4b2b0000+68000]
[Tue Apr 14 08:47:12 2025] php-fpm8.2[14523]: code: 0f 0b 0f 0b 48 8b 45 c8 48 85 c0 0f 84 a7 00 00

La riga in opcache.so è la firma inconfondibile: il crash avviene dentro il modulo OPcache, non nel codice applicativo. Il passo successivo è stato verificare quando PHP era stato aggiornato:

# Quando è stato aggiornato PHP?
grep "php8.2" /var/log/apt/history.log | tail -5
# Output: 2025-04-14 03:45:12 upgrade php8.2-fpm:amd64 8.2.18-1+deb12u1 -> 8.2.21-1+deb12u1

L'aggiornamento era avvenuto alle 03:45, sei ore prima della chiamata. Il sito aveva funzionato per qualche ora con la nuova versione - i segfault di OPcache spesso sono intermittenti e si manifestano sotto carico, quando il file cache tenta di scrivere script precompilati in modo concorrente tra i worker FPM.

I cinque errori PHP critici più frequenti su VPS e come risolverli

Nella mia esperienza su VPS di PMI, gli errori PHP critici ricadono in cinque categorie. Li elenco in ordine di frequenza, dal più comune al più raro.

Memory exhausted: il classico Allowed memory size

L'errore PHP Fatal error: Allowed memory size of X bytes exhausted è il più comune in assoluto. Significa che uno script PHP ha tentato di allocare più memoria di quanto consentito dal parametro memory_limit in php.ini. La soluzione immediata è aumentare il limite, ma la soluzione corretta è capire perché lo script consuma così tanta memoria.

# Trovare il memory_limit attuale
php -i | grep memory_limit

# Trovare QUALE file php.ini sta usando PHP-FPM
php-fpm8.2 -i | grep "Loaded Configuration File"

# Aumentare temporaneamente (fix immediato)
sed -i 's/^memory_limit = .*/memory_limit = 512M/' /etc/php/8.2/fpm/php.ini
systemctl restart php8.2-fpm

Il memory_limit di default su Debian 12 è 128M, che per un e-commerce Laravel con catalogo di migliaia di prodotti è insufficiente. Il mio standard per applicazioni Laravel in produzione è 256M per le richieste web e 512M per i processi CLI (artisan, queue worker). Se uno script ha bisogno di più di 512M, il problema non è il limite - è il codice che fa qualcosa di sbagliato (tipicamente carica in memoria un'intera tabella del database con Model::all() invece di usare chunk o cursor).

PHP-FPM in crash loop: worker che muoiono e rinascono

Quando PHP-FPM mostra nel log WARNING: [pool www] child XXXX exited on signal 11 (SIGSEGV) e subito dopo NOTICE: [pool www] child YYYY started, i worker stanno crashando e venendo riavviati ciclicamente. Il sito funziona a singhiozzo - alcune richieste passano, altre restituiscono schermata bianca - perché dipende da quale worker le gestisce e se crasha prima di completare la risposta.

# Contare quanti segfault ci sono stati nell'ultima ora
journalctl -u php8.2-fpm --since "1 hour ago" | grep -c "SIGSEGV"

# Se il colpevole è OPcache (come nel caso toscano), disabilitare il JIT
# Il JIT (Just-In-Time compiler) è la causa più frequente di segfault su PHP 8.x
sed -i 's/^opcache.jit=.*/opcache.jit=disable/' /etc/php/8.2/fpm/conf.d/10-opcache.ini
systemctl restart php8.2-fpm

# Se i segfault persistono, disabilitare anche il file cache di OPcache
sed -i 's/^opcache.file_cache=.*/;opcache.file_cache=/' /etc/php/8.2/fpm/conf.d/10-opcache.ini
systemctl restart php8.2-fpm

Il JIT di PHP 8.x è una feature potente ma ancora instabile su certi carichi di lavoro - la documentazione PHP stessa avverte che è sperimentale su alcune architetture. Il mio approccio è: JIT disabilitato di default su VPS di produzione, abilitato solo dopo test specifici che confermino un vantaggio misurabile senza crash.

Permessi rotti: il Permission denied silenzioso

Gli errori di permessi sono subdoli perché possono manifestarsi come schermata bianca (se display_errors è off) o come funzionalità parzialmente rotte (un upload che fallisce, una cache che non si scrive, un log che non registra). La causa è quasi sempre un deploy fatto come root che ha cambiato l'ownership dei file da www-data a root:

# Fix permessi standard per applicazione Laravel
chown -R www-data:www-data /var/www/html
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;

# Directory che devono essere scrivibili
chmod -R 775 /var/www/html/storage
chmod -R 775 /var/www/html/bootstrap/cache

Max execution time: script che impiegano troppo

L'errore Maximum execution time of 30 seconds exceeded significa che uno script PHP ha superato il tempo massimo consentito. Il fix immediato è aumentare max_execution_time, ma attenzione: aumentarlo troppo (600 secondi, 900 secondi) è un anti-pattern che nasconde problemi di performance. Se uno script impiega più di 60 secondi per una richiesta web, probabilmente dovrebbe essere spostato in una coda asincrona (Laravel queue) invece di essere eseguito inline.

OOM kill: il kernel uccide PHP-FPM

Quando la RAM è esaurita, il kernel Linux attiva l'OOM killer e termina il processo che consuma più memoria - che su un VPS LEMP è quasi sempre PHP-FPM o MySQL. Il sintomo è un'interruzione improvvisa del servizio senza errori PHP visibili, con la riga Out of memory: Killed process in dmesg.

# Verificare se PHP-FPM è stato killato per OOM
dmesg -T | grep -i "out of memory"

# Calcolare quanta RAM usa PHP-FPM (per worker)
ps aux | grep php-fpm | awk '{sum += $6} END {print sum/1024 " MB totali, " NR " processi"}'

# Ridurre il numero di worker se la RAM è insufficiente
# In /etc/php/8.2/fpm/pool.d/www.conf:
# pm = dynamic
# pm.max_children = 5    (su VPS con 4GB, non di più)
# pm.start_servers = 2
# pm.min_spare_servers = 1
# pm.max_spare_servers = 3

La formula che uso per calcolare pm.max_children è: (RAM totale - RAM MySQL - RAM sistema) / RAM media per worker PHP. Su un VPS con 4 GB, MySQL tipicamente usa 1-1.5 GB, il sistema 500 MB, e ogni worker PHP-FPM usa 40-80 MB. Il calcolo: (4096 - 1200 - 500) / 60 ≈ 40 worker massimi in teoria, ma in pratica ne imposto 5-8 per lasciare margine a picchi di utilizzo. Ho descritto in dettaglio il tuning di PHP-FPM nell'articolo sull'ottimizzazione performance PHP su Hetzner e OVH.

Il protocollo completo che uso per ogni emergenza PHP

Quando ricevo una chiamata per un "sito che non funziona" su un VPS con PHP, seguo sempre questa sequenza - senza eccezioni, senza scorciatoie:

1. dmesg -T | tail -30 - cercare segfault, OOM, errori hardware

  1. systemctl status php8.2-fpm - il servizio è attivo? quanti worker?

3. journalctl -u php8.2-fpm --since "1 hour ago" | tail -30 - errori FPM

  1. tail -30 /var/log/nginx/error.log - errori dal reverse proxy
  2. tail -30 /var/www/html/storage/logs/laravel.log - errori applicativi
  3. df -h - disco pieno?
  4. free -h - RAM esaurita?
  5. php -v - versione PHP cambiata?

Otto comandi, meno di un minuto, e nel 95% dei casi ho già identificato la causa radice. L'ordine non è casuale: parto dal livello più basso (kernel) e salgo fino all'applicazione, perché gli errori a livello di kernel sono invisibili a tutti i livelli superiori e se non li cerchi per primi perdi ore a guardare nel posto sbagliato.

Per un'analisi più profonda dell'osservabilità delle applicazioni PHP, inclusa la configurazione di logging strutturato e metriche che rendono queste diagnosi molto più rapide, ho scritto un articolo dedicato.

Prevenzione: come evitare che un aggiornamento PHP rompa la produzione

Il caso toscano è stato causato da un aggiornamento automatico - unattended-upgrades ha installato la nuova versione di PHP alle 3 di notte e nessuno ha verificato che tutto funzionasse. Questo pattern è estremamente comune su VPS di PMI: gli aggiornamenti di sicurezza sono attivi (giustamente), ma nessuno controlla il risultato.

Il mio approccio per i pacchetti critici - PHP, MySQL, Nginx - è un modello a tre livelli. Primo: pinnare la versione corrente con apt-mark hold per impedire aggiornamenti non controllati:

# Bloccare gli aggiornamenti automatici di PHP
apt-mark hold php8.2-fpm php8.2-cli php8.2-common php8.2-mysql php8.2-redis

# Verificare quali pacchetti sono bloccati
apt-mark showhold

# Quando vuoi aggiornare manualmente
apt-mark unhold php8.2-fpm php8.2-cli php8.2-common
apt update && apt upgrade -y
# TEST IMMEDIATO: verificare che il sito funzioni
curl -s -o /dev/null -w "%{http_code}" https://www.esempio.it/health-check
apt-mark hold php8.2-fpm php8.2-cli php8.2-common

Secondo: un health check automatico post-aggiornamento. Anche quando gli aggiornamenti sono bloccati, i pacchetti di sistema (librerie, kernel) possono influire indirettamente su PHP. Il mio script di health check gira ogni 5 minuti e verifica che PHP-FPM risponda correttamente:

# Cron: health check PHP ogni 5 minuti
*/5 * * * * curl -sf -o /dev/null --max-time 5 http://127.0.0.1/health-check || \
    (systemctl restart php8.2-fpm && echo "PHP-FPM restarted $(date)" >> /var/log/php-health.log)

Terzo: quando effettuo l'aggiornamento manuale, lo faccio in una finestra di manutenzione con rollback pronto. Su Debian, apt mantiene i vecchi pacchetti in cache, quindi il downgrade è possibile con apt install php8.2-fpm=8.2.18-1+deb12u1 - a patto di conoscere la versione precedente. Lo annoto sempre nel runbook del server.

Per una visione completa sulla gestione dei rischi del codice PHP obsoleto e i vantaggi degli aggiornamenti controllati, ho scritto un articolo dedicato che copre anche il lato applicativo della questione.

Il caso toscano si è risolto in venti minuti: disabilitato il JIT di OPcache, riavviato PHP-FPM, verificato che il sito funzionasse, e pinnato la versione PHP in apt per impedire aggiornamenti automatici non controllati (apt-mark hold php8.2-fpm php8.2-cli php8.2-common). La lezione - che vale per ogni VPS in produzione - è che gli aggiornamenti automatici di sicurezza sono fondamentali ma devono essere monitorati. Un unattended-upgrades senza un test post-aggiornamento è una scommessa che il nuovo pacchetto non rompa nulla. Per pacchetti critici come PHP, il mio approccio è: aggiornamento manuale con test, non automatico con preghiera. Se gestisci un VPS con applicazioni PHP in produzione e vuoi prevenire questo tipo di incidenti, o se stai affrontando un'emergenza adesso, contattami.

Ultima modifica: