Impilare Reverse Proxy: 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 distribuzioni client: Cloudflare all'edge, Traefik come punto di ingresso consapevole dei 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 crea overhead 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 consapevole dei container all'interno della tua infrastruttura. When you run a multi-tenant Docker stack, 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 alla scalatura dei container. Gestisce i certificati TLS da Let's Encrypt per i domini interni o di staging, 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 sono memorizzabili nella cache per utente, 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 fare proxy verso un processo applicativo upstream. CloudPanel uses Nginx as its managed web server, meaning each vhost has an auto-generated configuration that handles PHP-FPM, static asset serving, and gzip compression. Quella configurazione è mantenuta da CloudPanel e non dovrebbe essere modificata direttamente. Nginx qui non è un livello decisionale — riceve una richiesta, la fa proxy 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 standard Traefik examples show a direct-to-app pattern, and grafting Varnish into that flow is not documented well.
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 imprime l'header prima che la richiesta entri nella rete interna, Varnish lo passa intatto e Nginx lo inoltra all'applicazione. L'header companion 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 fallirà silenziosamente in modi difficili da tracciare.
VCL di Varnish: Logica della Cache
Il VCL seguente è un punto di partenza funzionante per un'applicazione WordPress o PHP dietro CloudPanel. 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 per prevenire le cache stampede.
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. Cloudflare elimina alcuni parametri all'edge, ma 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 fredde ed elimina completamente il beneficio del rapporto di hit.
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, ogni richiesta viene inoltrata a Nginx contemporaneamente. Con un periodo di grazia di 60 secondi, Varnish serve l'oggetto obsoleto a tutte le richieste tranne una, mentre un singolo fetch in background riconvalida la cache. Il risultato è che Nginx gestisce una richiesta invece di centinaia al momento della scadenza.
Implicazioni sulle Prestazioni
Le prestazioni di questo stack non sono uniformi. Ci sono punti in cui eccelle e punti in cui aggiunge overhead misurabile.
Dove performa bene: 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. Il serving 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 aggiunge overhead: i miss della cache attraversano ora quattro hop invece di uno. Una richiesta PHP che non può essere memorizzata nella cache — una sessione utente connesso, un invio POST, una pagina carrello WooCommerce — passa attraverso Traefik, Varnish e Nginx prima di raggiungere PHP-FPM. Ogni hop di 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. La configurazione predefinita usa 256MB. Per un sito con molti URL unici questo si riempie rapidamente, il tasso di eviction aumenta e il rapporto di hit della cache rimane basso indipendentemente dalle impostazioni TTL. Il primo fattore di prestazione da regolare dopo la distribuzione è 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: TLS termina a Cloudflare. In una configurazione con Cloudflare come front-end, i segmenti da Traefik a Varnish e da Varnish a Nginx vengono eseguiti su HTTP normale sulla rete bridge interna Docker. Questo è intenzionale. Aggiungere TLS alla comunicazione interna tra container sullo stesso host introduce overhead CPU senza un miglioramento della sicurezza significativo — il modello di minaccia per il traffico del bridge Docker è categoricamente diverso dal traffico internet pubblico. Se il tuo modello di minaccia richiede comunicazione crittografata tra container, questa è una diversa conversazione architetturale.
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. Lo stack a quattro livelli guadagna la sua complessità 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 multi-service deployments where Traefik is already the routing layer for a dozen containers and adding Varnish for one of them is incremental cost, not a new system.
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 è di proprietà di 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 quale middleware ha eseguito e cosa ha ricevuto l'upstream; 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 routing, caching, serving dell'applicazione e calcolo — quattro preoccupazioni che si scalano indipendentemente e falliscono indipendentemente. Quel confine vale la superficie di configurazione aggiuntiva.
Approfondimenti Correlati
- Distribuire Applicazioni React in Produzione: Configurazione Docker Completa con Traefik Reverse Proxy
- Guida Completa alla Risoluzione dei Problemi di n8n Self-Hosted 2025: Correggere i Problemi di Dimensione dei Dati di Esecuzione e Webhook con Traefik
- Costruire uno Stack di Sviluppo Multi-Tenant con Docker: Configurazione Completa per Distribuzioni Client Scalabili
- Ottimizzazione delle Prestazioni di CloudPanel: Massimizzare le Prestazioni del Server Cloud Hetzner per una Consegna Rapidissima dei Siti Web