Reverse proxy in cascata: un’architettura per la produzione
Quattro reverse proxy davanti a una singola applicazione appaiono, a prima vista, come un sistema che necessita di semplificazione. La maggior parte dei tutorial sull'infrastruttura web in produzione si limita a un unico livello — Nginx, Caddy o Traefik — e considera qualsiasi cosa in più come un eccesso di ingegneria. In realtà, però, ciascuno di questi strumenti risolve un problema distinto e, quando si comprende la divisione del lavoro, la ridondanza apparente si trasforma in qualcosa di coerente.
Questa è l'architettura che utilizziamo per diverse installazioni per i clienti: Cloudflare all'edge, Traefik come punto di ingresso che riconosce i container, Varnish per il caching HTTP, Nginx come host dell'applicazione e l'applicazione stessa alla fine della catena. Il flusso delle richieste è:
Cloudflare → Traefik → Varnish → Nginx → Application
Ciò che segue spiega il contributo di ogni livello, dove si trovano i confini e quali sono le implicazioni sulle prestazioni — inclusi i punti in cui questo stack introduce costi aggiuntivi che una configurazione più semplice non avrebbe.
Cosa fa davvero ogni livello
Cloudflare opera all'edge della rete Internet pubblica. Assorbe il traffico DDoS, applica le regole WAF, termina TLS ai PoP distribuiti globalmente e memorizza nella cache gli asset statici vicino agli utenti finali. Ciò che non fa bene è prendere decisioni di routing intelligenti sui tuoi container, o memorizzare nella cache le risposte dell'applicazione che richiedono l'ispezione dei cookie. Questo è compito di qualcun altro.
Traefik è il punto di ingresso che riconosce i container all'interno della tua infrastruttura. Quando utilizzi uno stack Docker multi-tenant, il valore di Traefik sta nel leggere le etichette dei container e instradare di conseguenza — nessun file di configurazione manuale che si desincronizza al riavvio o all’aumento o alla riduzione del numero di container. Gestisce i certificati TLS da Let's Encrypt per i domini interni o di preproduzione, elimina o riscrive gli header e applica catene di middleware come l'allowlisting degli IP o l'autenticazione di base. Traefik non fa caching. Non comprime bene sotto carico. Il suo compito è il routing e il middleware, e lo svolge efficientemente.
Varnish è l'acceleratore HTTP. Si colloca tra il punto di ingresso e l'host dell'applicazione specificamente per assorbire il traffico in lettura. Un'istanza Varnish ben ottimizzata può servire decine di migliaia di richieste al secondo dalla memoria, restituendo pagine HTML complete in meno di un millisecondo. Il suo VCL (Varnish Configuration Language) offre un controllo preciso su cosa viene memorizzato nella cache, per quanto tempo e in quali condizioni — molto più granulare di qualsiasi regola di cache di Cloudflare. Questo è importante per le applicazioni autenticate in cui alcune risposte potrebbero essere memorizzate nella cache per utente con un calcolo hash personalizzato (l’esempio seguente non lo fa: inoltra il traffico degli utenti connessi senza usare la cache), o per le pagine che dovrebbero avere TTL diversi a seconda dei parametri di query.
Nginx, in questo stack, svolge il ruolo per cui è stato originariamente progettato: servire file e inoltrare le richieste a un processo applicativo upstream. CloudPanel usa Nginx come server web gestito: ogni vhost ha quindi una configurazione generata automaticamente che gestisce PHP-FPM, la distribuzione degli asset statici e la compressione gzip. Quella configurazione è mantenuta da CloudPanel e non dovrebbe essere modificata direttamente. Nginx qui non è un livello decisionale — riceve una richiesta, la inoltra all’applicazione e restituisce la risposta.
Configurazione di Traefik: etichette dei container
Il routing di Traefik per questo stack è configurato interamente tramite etichette di Docker Compose. L'intuizione chiave quando si inserisce Varnish dietro Traefik è che Traefik dovrebbe inoltrare al container Varnish, non direttamente a Nginx. È qui che la maggior parte delle persone si confonde: i esempi standard di Traefik mostrano un inoltro diretto all’applicazione e l’inserimento di Varnish in quel flusso è poco documentato.
services:
varnish:
image: varnish:7.5
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`www.example.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls=true"
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
- "traefik.http.services.myapp.loadbalancer.server.port=80"
- "traefik.http.middlewares.myapp-proto.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.middlewares.myapp-proto.headers.customrequestheaders.X-Forwarded-Port=443"
- "traefik.http.routers.myapp.middlewares=myapp-proto"
networks:
- traefik-public
- internal
L'header X-Forwarded-Proto è fondamentale. Senza di esso, la tua applicazione vede ogni richiesta come HTTP — poiché la comunicazione da Varnish a Nginx avviene su HTTP normale sulla rete interna Docker — causando loop di redirect in qualsiasi applicazione che applica HTTPS. Traefik imposta l’header prima che la richiesta entri nella rete interna, Varnish lo passa intatto e Nginx lo inoltra all'applicazione. L’header complementare X-Forwarded-Port previene la stessa classe di loop di redirect nelle applicazioni che ispezionano anche la porta.
Una seconda considerazione: l'impostazione passHostHeader di Traefik è true per impostazione predefinita, il che significa che Varnish riceve l'header Host originale. Questo è ciò che vuoi — il VCL di Varnish può usarlo per la chiave della cache, e Nginx lo usa per la selezione del vhost. Se sovrascrivi questa impostazione, sia Varnish che Nginx riceveranno l’host errato e presenteranno errori silenziosi difficili da individuare.
VCL di Varnish: logica della cache
Il VCL seguente è un punto di partenza per un’applicazione WordPress o PHP dietro CloudPanel, non una policy pronta per la produzione. Copre i casi comuni: bypass per gli utenti connessi e le richieste non idempotenti, caching esteso per gli asset statici, normalizzazione dei parametri di tracciamento e un periodo di grazia che consente di servire pagine leggermente obsolete mentre Varnish le aggiorna. Prima della messa in produzione, escludi dalla cache anche le pagine del carrello, del checkout e dell’account di WooCommerce e il cookie wp_woocommerce_session_. Poiché questo vcl_backend_response termina con return(deliver), non viene eseguita la logica integrata di Varnish che impedisce di memorizzare nella cache le risposte con Set-Cookie o Cache-Control: private/no-store: aggiungi quindi controlli equivalenti.
vcl 4.1;
backend default {
.host = "nginx"; # nome del servizio in docker-compose
.port = "80";
.connect_timeout = 5s;
.first_byte_timeout = 30s;
.between_bytes_timeout = 10s;
}
sub vcl_recv {
# Passa le richieste admin e autenticate
if (req.url ~ "^/wp-(admin|login|cron|json)" ||
req.http.Cookie ~ "wordpress_logged_in_" ||
req.http.Cookie ~ "woocommerce_items_in_cart") {
return(pass);
}
# Memorizza nella cache solo GET e HEAD
if (req.method != "GET" && req.method != "HEAD") {
return(pass);
}
# Normalizza i parametri di tracciamento dalla chiave della cache
if (req.url ~ "[?&](utm_source|utm_medium|utm_campaign|fbclid|gclid)=") {
set req.url = regsuball(req.url,
"&(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+", "");
set req.url = regsuball(req.url,
"\?(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+&", "?");
set req.url = regsub(req.url,
"\?(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+$", "");
}
# Rimuovi i cookie per gli asset statici — abilita il caching
if (req.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|woff2?|svg|webp)(\?.*)?$") {
unset req.http.Cookie;
}
return(hash);
}
sub vcl_backend_response {
# Memorizza nella cache gli asset statici per 7 giorni
if (bereq.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|woff2?|svg|webp)(\?.*)?$") {
set beresp.ttl = 7d;
set beresp.grace = 24h;
unset beresp.http.Set-Cookie;
return(deliver);
}
# Memorizza nella cache le pagine HTML per 5 minuti con un periodo di grazia di 60 secondi
if (beresp.status == 200 && beresp.http.Content-Type ~ "text/html") {
set beresp.ttl = 300s;
set beresp.grace = 60s;
}
# Non memorizzare mai le risposte di errore
if (beresp.status >= 500) {
set beresp.ttl = 0s;
}
return(deliver);
}
sub vcl_deliver {
# Esponi lo stato della cache per il debug
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
set resp.http.X-Cache-Hits = obj.hits;
} else {
set resp.http.X-Cache = "MISS";
}
# Rimuovi gli header di identità del server
unset resp.http.Via;
unset resp.http.X-Varnish;
return(deliver);
}
La normalizzazione dei parametri di tracciamento in vcl_recv è facile da trascurare e costosa da ignorare. A meno di configurare la riscrittura degli URL su Cloudflare, i valori UTM e i token fbclid arrivano a Varnish con l'URL originale intatto. Senza normalizzazione, ogni clic su un annuncio Facebook genera una voce di cache unica per ciò che, dalla prospettiva dell'applicazione, è la stessa pagina. Nel corso di un periodo di campagna, questo frammenta la cache di Varnish in migliaia di voci poco utilizzate ed elimina completamente il beneficio del tasso di riscontri in cache.
L'impostazione grace in vcl_backend_response è l'altro meccanismo che vale la pena capire in dettaglio. Quando il TTL di un oggetto memorizzato nella cache scade, la richiesta successiva di solito aspetterebbe che Nginx restituisca una risposta aggiornata prima che Varnish consegni qualcosa. In caso di picco di traffico — un invio di newsletter, una menzione sui social media — più richieste arrivano simultaneamente per la voce scaduta. Senza grace, Varnish raggruppa comunque le richieste per un oggetto memorizzabile nella cache in un singolo fetch verso Nginx, ma le altre richieste attendono che questo sia completato. Con un periodo di grazia di 60 secondi, Varnish serve subito l’oggetto obsoleto a queste richieste mentre un singolo fetch in background aggiorna la cache. Il risultato è che Nginx gestisce una richiesta invece di centinaia al momento della scadenza e i visitatori non devono attendere.
Implicazioni sulle prestazioni
Le prestazioni di questo stack non sono uniformi. Ci sono punti in cui eccelle e punti in cui aggiunge costi misurabili.
Dove offre buone prestazioni: qualsiasi pagina memorizzabile nella cache servita dalla memoria di Varnish viene restituita in meno di 2ms a livello Traefik. Una homepage WordPress che impiega 400ms per essere generata da PHP viene servita in 1–2ms per ogni richiesta successiva fino alla scadenza del TTL. Il miglioramento del throughput non è incrementale — è di un ordine di grandezza. La distribuzione degli asset statici tramite Nginx è veloce in ogni caso, e Cloudflare memorizza quegli asset nella cache all'edge comunque, quindi Nginx raramente li gestisce in produzione.
Dove introduce costi aggiuntivi: le richieste non soddisfatte dalla cache attraversano ora quattro passaggi invece di uno. Una richiesta PHP che non può essere memorizzata nella cache — la sessione di un utente autenticato, un invio POST, una pagina carrello WooCommerce — passa attraverso Traefik, Varnish e Nginx prima di raggiungere PHP-FPM. Ogni passaggio sulla rete Docker sullo stesso host aggiunge circa 0,1–0,3ms di latenza. Per una risposta PHP da 50ms, questo è accettabile. Per un endpoint API che dovrebbe rispondere in meno di 5ms, non lo è. In quel caso, l'approccio corretto è instradare il percorso API direttamente al container dell'applicazione tramite una regola router Traefik separata, bypassando completamente Varnish.
Il dimensionamento della memoria è più importante di quasi tutti gli altri parametri: Varnish alloca il suo archivio oggetti dalla RAM. L’immagine Docker ufficiale usa 100 MB per impostazione predefinita. Per un sito con molti URL unici questo si riempie rapidamente, il tasso di espulsione degli oggetti aumenta e il tasso di riscontri in cache rimane basso indipendentemente dalle impostazioni TTL. Il primo fattore di prestazione da regolare dopo la messa in produzione è aumentare l'allocazione — il flag di avvio -s malloc,2G, o la variabile d'ambiente VARNISH_SIZE=2G in Docker — e monitorare il risultato con varnishstat -1 | grep hit.
Terminazione SSL: Il TLS pubblico termina su Cloudflare. Con la modalità SSL «Full (strict)» di Cloudflare, quest’ultimo si connette poi a Traefik tramite HTTPS usando il certificato Let’s Encrypt del router mostrato sopra. Dietro Traefik, i segmenti da Traefik a Varnish e da Varnish a Nginx usano HTTP non crittografato sulla rete bridge interna Docker. Questo è intenzionale. Su un singolo host in cui controlli tutti i container, aggiungere TLS alla comunicazione interna tra container aumenta il carico sulla CPU con un vantaggio di sicurezza limitato, perché il traffico del bridge non lascia l’host; non ti proteggerebbe da un container o un host compromesso. Se il tuo modello di minaccia richiede comunicazione crittografata tra container, occorre una valutazione architetturale diversa.
Quando questo stack ha senso
Questa non è una raccomandazione generale. Per una singola applicazione containerizzata senza requisiti di caching e senza il vincolo di CloudPanel, il routing di Traefik direttamente all'app è più semplice e corretto. La complessità dello stack a quattro livelli è giustificata in scenari specifici: ambienti gestiti da CloudPanel dove Nginx non è negoziabile, siti basati su CMS dove il caching full-page fornisce miglioramenti del throughput di un ordine di grandezza, o installazioni con più servizi in cui Traefik gestisce già il routing di una dozzina di container e aggiungere Varnish per uno di essi comporta un costo incrementale, senza introdurre un nuovo sistema.
La separazione delle responsabilità è ciò che rende la complessità gestibile nel tempo. La configurazione di Traefik vive nelle etichette Docker Compose. La logica di Varnish vive nei file VCL versionati insieme allo stack compose. La configurazione di Nginx è gestita da CloudPanel. Nessuno di questi sistemi entra nel dominio degli altri. Quando qualcosa si comporta inaspettatamente, il percorso diagnostico è chiaro: l'header X-Cache dice se Varnish ha servito la risposta; il log degli accessi di Traefik registra il router, il servizio backend e i codici di stato restituiti dall’upstream e inviati al client; il log degli errori di Nginx registra cosa ha riportato PHP-FPM. Ogni livello produce il proprio segnale osservabile, e i fallimenti non si accumulano silenziosamente tra i livelli.
Ciò che sembra ridondanza è, in pratica, un confine chiaro tra protezione all’edge, routing, caching e gestione delle richieste applicative: quattro responsabilità che possono scalare e subire guasti indipendentemente. Questa separazione giustifica la configurazione aggiuntiva.
Approfondimenti correlati
- Distribuire applicazioni React in produzione: configurazione Docker completa con Traefik come reverse proxy
- Guida completa alla risoluzione dei problemi di n8n sulla propria infrastruttura 2025: correggere i problemi di dimensione dei dati di esecuzione e dei webhook con Traefik
- Costruire uno stack di sviluppo multi-tenant con Docker: configurazione completa per installazioni scalabili dei clienti
- Ottimizzazione delle prestazioni di CloudPanel: massimizzare le prestazioni del server cloud Hetzner per servire i siti web a grande velocità