SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-13

Nginx, Caddy o Traefik: quale reverse proxy scegliere

Confronta Nginx, Caddy e Traefik su un VPS: certificati TLS, configurazione per app, WebSocket e routing Docker con un solo IP pubblico.

Nginx vs Caddy vs Traefik: la risposta breve

Nginx, Caddy e Traefik svolgono lo stesso ruolo di reverse proxy: restano in ascolto sulla porta 443, leggono il nome host di ogni richiesta e la inoltrano al servizio corretto sul tuo VPS. Ognuno dei tre può pubblicare quattro applicazioni self-hosted usando un unico indirizzo IP pubblico, e tutti sono abbastanza veloci da fare in modo che il collo di bottiglia siano le applicazioni. Le differenze riguardano il modo in cui ciascuno ottiene un certificato TLS (transport layer security) e il carico di configurazione richiesto da ogni applicazione aggiuntiva. L’altra differenza emerge più avanti, quando devi gestire un caso che i tutorial più comuni non trattano.

Scegli Caddy se vuoi che HTTPS venga gestito automaticamente e i tuoi servizi sono normali applicazioni web. Scegli Traefik se tutto viene eseguito con Docker Compose e aggiungi un nuovo servizio ogni poche settimane. Scegli Nginx se lo utilizzi già oppure se ti servono il caching delle risposte, i certificati client, l’inoltro TCP grezzo o una configurazione esistente estesa che preferisci non riscrivere.

Come ottiene ciascuno un certificato TLS?

Questo criterio è decisivo per la maggior parte delle persone, quindi conviene partire da qui. Alla fine, tutte e tre le soluzioni usano lo stesso certificato rilasciato dalla stessa autorità. Il percorso per ottenerlo, però, è diverso.

Caddy richiede il certificato perché hai indicato un hostname. Scrivi app.example.com come indirizzo del sito e Caddy richiede un certificato tramite ACME (automatic certificate management environment) a Let's Encrypt, ripiega su ZeroSSL se la richiesta non riesce, serve il redirect da HTTP a HTTPS sulla porta 80 e rinnova il certificato automaticamente. Non servono un secondo strumento né un timer da verificare. I certificati vengono salvati nella directory dati dell'utente caddy, /var/lib/caddy/.local/share/caddy in un'installazione tramite pacchetto. Aggiungi quindi questo percorso ai backup oppure considera la possibilità di una nuova emissione dopo una ricostruzione. Per un hostname non pubblico, tls internal firma il certificato con la propria autorità di certificazione locale. Il risultato è equivalente a creare un certificato autofirmato su Ubuntu, ma il rinnovo viene gestito automaticamente.

Nginx non include un client ACME. Certbot ottiene il certificato e il suo plugin --nginx modifica il server block per aggiungere il listener sulla porta 443 e il redirect. Il rinnovo viene eseguito da un timer systemd installato dal pacchetto. Ci sono quindi due componenti e due verifiche da effettuare: systemctl list-timers | grep certbot mostra che il timer esiste e sudo certbot renew --dry-run conferma che il percorso di rinnovo funziona ancora. La procedura dettagliata è disponibile in Certbot su Ubuntu 24.04 con Nginx. Lo stesso strumento gestisce anche un certificato wildcard tramite la challenge DNS-01 quando hai più sottodomini di quanti ne vuoi elencare.

Traefik include un proprio client ACME. Configura un certificate resolver nella configurazione statica. Ogni router può quindi utilizzarlo. Tutto lo stato, inclusi la chiave dell'account e i certificati, viene salvato in un unico file acme.json. Traefik rifiuta di utilizzare questo file se è leggibile da utenti diversi dal proprietario e lo segnala prima di disattivare il resolver:

The ACME resolver "le" is skipped from the resolvers list because: unable to get ACME account: permissions 660 for /letsencrypt/acme.json are too open, please use 600

Monta una directory e lascia che Traefik crei direttamente il file. Crealo prima con touch. In questo modo eredita il tuo umask, ed è così che la maggior parte delle persone soddisfa questo requisito.

Una condizione vale per tutte e tre le soluzioni. La challenge HTTP-01 richiede che la porta 80 sia raggiungibile da Internet, perché l'autorità di certificazione deve connettersi al server. Se apri soltanto la porta 443, l'emissione fallisce con un errore che sembra indicare un problema DNS.

Lo stesso instradamento per 2 applicazioni in 3 configurazioni

Il compito è il seguente: app.example.com deve raggiungere un servizio su 127.0.0.1:8080, mentre files.example.com deve raggiungere un servizio su 127.0.0.1:8081, entrambi tramite HTTPS. Di seguito è riportata la configurazione completa per ciascun proxy, così la differenza di verbosità è evidente e non soltanto dichiarata.

Nginx

# /etc/nginx/sites-available/app.example.com
server {
    listen 80;
    server_name app.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Collegatela, verificatela, ricaricate la configurazione e aggiungete il certificato.

sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d app.example.com

L'output di nginx -t con syntax is ok e test is successful è il controllo da eseguire prima di ogni reload. La seconda applicazione usa lo stesso blocco, modificando hostname e porta. Le righe proxy_set_header non sono decorative: quando proxy_pass indica un indirizzo, nginx invia per impostazione predefinita Host: 127.0.0.1:8080 al servizio upstream. In caso contrario, un'applicazione che genera URL assoluti dall'header Host reindirizzerebbe gli utenti a localhost.

Caddy

app.example.com {
	reverse_proxy 127.0.0.1:8080
}

files.example.com {
	reverse_proxy 127.0.0.1:8081
}
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

Questo è l'intero file. reverse_proxy imposta autonomamente X-Forwarded-For, X-Forwarded-Proto e X-Forwarded-Host. Per impostazione predefinita ignora i valori inviati dal client in questi header, quindi una richiesta non può fornire al backend informazioni false sulla propria origine. I certificati, il redirect dalla porta 80 e il rinnovo derivano dai 2 indirizzi dei siti. Nel file non serve altro per attivarli.

Traefik

Traefik richiede una configurazione statica prima di poter eseguire qualsiasi instradamento. Come servizio Compose, con il tag dell'immagine aggiornato ad agosto 2026:

services:
  traefik:
    image: traefik:v3.7
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.le.acme.email=you@example.com"
      - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt

Ogni applicazione definisce quindi il proprio instradamento tramite label, nel proprio file Compose:

    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.app.rule=Host(`app.example.com`)"
      - "traefik.http.routers.app.entrypoints=websecure"
      - "traefik.http.routers.app.tls.certresolver=le"
      - "traefik.http.services.app.loadbalancer.server.port=8080"

loadbalancer.server.port è la porta interna al container, non una porta pubblicata, perché Traefik raggiunge il container tramite una rete Docker condivisa. L'applicazione non richiede alcuna riga ports:, ed è questo il vantaggio principale: solo Traefik viene pubblicato. La configurazione completa, inclusi la rete condivisa e il middleware di redirect, è descritta in instradare più applicazioni con Traefik e Docker Compose.

Quanto costa in configurazione ogni applicazione aggiuntiva?

ChartNon-blank config lines for the same two-app routing job
The data behind this chart
[
  {
    "tool": "Nginx",
    "proxy_setup_lines": 0,
    "lines_per_app": 11
  },
  {
    "tool": "Caddy",
    "proxy_setup_lines": 0,
    "lines_per_app": 3
  },
  {
    "tool": "Traefik",
    "proxy_setup_lines": 17,
    "lines_per_app": 5
  }
]

I conteggi derivano dai blocchi precedenti. Il blocco server di Nginx contiene 11 righe non vuote e va riscritto per ogni hostname. Il blocco site di Caddy contiene 3 righe. Prima di poter gestire anche una sola richiesta, Traefik richiede 17 righe di configurazione statica; poi richiede 5 label per applicazione.

Valuta il compromesso, non soltanto quale soluzione vince. Traefik richiede il maggior numero di configurazioni prima della prima applicazione e il minor numero per ogni applicazione successiva. I due totali si equivalgono intorno al terzo sito. Al di sotto di questa soglia, la configurazione statica è un sovraccarico non necessario. Al di sopra, le label diventano più convenienti e il vantaggio aumenta, perché il routing si trova accanto al servizio a cui si riferisce. Se elimini il servizio, elimini anche la relativa route. È proprio questo il limite di un file di configurazione centralizzato: lasciare blocchi server obsoleti per applicazioni che non esistono più da mesi.

Il conteggio delle righe favorisce inoltre Nginx. Per ciascuno di questi blocchi servono un symlink, un nginx -t, un reload e l'esecuzione di certbot. La modifica in Caddy richiede un solo reload, mentre quella in Traefik non richiede alcun comando. Tutte e tre le soluzioni eseguono il reload senza interrompere le connessioni attive. La differenza sta nel numero di passaggi distinti da ricordare all'una di notte.

Quale conosce i tuoi container?

Traefik monitora il socket Docker e crea i router a partire dalle label dei container quando questi vengono avviati o arrestati. Nessun altro componente qui svolge questa funzione. Nginx e Caddy richiedono entrambi la modifica della configurazione e un reload quando compare un nuovo container. Richiedono inoltre un indirizzo raggiungibile: una porta pubblicata su loopback oppure una rete Docker condivisa a cui sia collegato anche il proxy.

Questa funzione ha un costo, che è importante esplicitare. Traefik legge /var/run/docker.sock. Chiunque possa comunicare con quel socket può avviare un container con il filesystem dell'host montato al suo interno, ottenendo così l'accesso root sull'host. Il montaggio in sola lettura riduce il rischio, ma non lo elimina. Se questo aspetto è rilevante per il tuo modello di minaccia, inserisci un socket proxy che esponga soltanto gli endpoint per l'elenco dei container necessari a Traefik.

Caddy può eseguire il rilevamento basato sulle label tramite un plugin della community, ma i plugin di Caddy vengono inclusi in fase di compilazione. Devi quindi creare un binario personalizzato o un'immagine personalizzata con xcaddy, assumendoti poi la gestione di quella build e dei relativi aggiornamenti. Per tre o quattro servizi, modificare un Caddyfile richiede meno lavoro.

WebSocket e streaming: cosa si rompe e perché

È nginx ad avere bisogno di una configurazione aggiuntiva. Una connessione WebSocket inizia come una richiesta HTTP che contiene Upgrade: websocket, ma nginx non inoltra a monte gli header hop-by-hop se non glielo si indica esplicitamente.

# /etc/nginx/conf.d/upgrade-map.conf
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

Quindi, all'interno del blocco location, devono essere presenti tutte e tre queste righe:

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

Se le ometti, la console del browser mostra WebSocket connection to 'wss://app.example.com/ws' failed, mentre il log del backend registra una normale richiesta GET. Il valore map è necessario perché un Connection: upgrade codificato staticamente verrebbe inviato per ogni richiesta, comprese quelle normali che dovrebbero indicare close.

Anche altre due impostazioni predefinite di Nginx possono causare problemi. proxy_read_timeout è di 60 secondi e si applica al tunnel dopo l'upgrade, quindi il proxy chiude una connessione WebSocket che non trasferisce dati per un minuto. Gli eventi server-sent arrivano in ritardo o a raffiche finché non imposti proxy_buffering off; nella relativa location, perché nginx mantiene la risposta nel buffer mentre la pagina attende i dati.

Caddy esegue l'upgrade e converte la connessione in un tunnel bidirezionale senza richiedere direttive. Inoltre esegue il flush immediatamente quando la risposta è text/event-stream o quando la lunghezza non è nota, quindi lo streaming funziona senza modifiche. Traefik inoltra gli upgrade e non memorizza le risposte nel buffer, a meno che tu non aggiunga manualmente il middleware buffering. Se i tuoi servizi includono chat, un terminale web, il monitoraggio continuo dei log o dashboard in tempo reale, questa differenza incide concretamente sulla quantità di configurazione da scrivere e sottoporre a troubleshooting.

Il blocco server completo di Nginx, con WebSocket e SSE inclusi
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name app.example.com;
    client_max_body_size 64m;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 3600s;
        proxy_buffering off;
    }
}

map deve trovarsi nel contesto http, non all'interno di server, quindi lascialo in un file separato sotto /etc/nginx/conf.d/. Disattiva proxy_buffering soltanto nelle location che eseguono lo streaming, perché il buffering consente a nginx di liberare rapidamente il worker del backend nelle risposte normali. Certbot riscrive questo blocco quando lo esegui, quindi rileggi il file al termine.

Cosa succede quando serve qualcosa di insolito?

È in questi casi che Nginx giustifica le righe di configurazione aggiuntive.

  • Certificati client, chiamati anche mTLS (mutual TLS), in cui anche il client deve presentare un certificato. Nginx richiede ssl_client_certificate /etc/ssl/ca.pem; e ssl_verify_client on; nel blocco server. Caddy richiede un blocco client_auth all'interno di tls. Le label di Traefik non possono esprimere questa configurazione: si definisce un'opzione TLS in un file provider e si collega il router tramite traefik.http.routers.app.tls.options=mtls@file. Il modello basato interamente sulle label prevede un'eccezione non appena serve questa funzione.
  • Upload di grandi dimensioni. Per impostazione predefinita, Nginx limita i body delle richieste a 1 MB. Un upload più grande restituisce 413 Request Entity Too Large e il log degli errori contiene client intended to send too large body. Aumentare client_max_body_size. Caddy e Traefik non impostano alcun limite predefinito sul body, quindi la richiesta raggiunge l'applicazione e il limite dell'applicazione determina il risultato.
  • Caching delle risposte. Nginx dispone di proxy_cache, una funzionalità consolidata. Caddy richiede un plugin incluso nella compilazione. La build open source di Traefik non include alcuna cache HTTP, un aspetto che sorprende chi presume che ogni proxy esegua il caching.
  • TCP o UDP grezzo, ad esempio per la porta di un database o per un game server. Nginx dispone del modulo stream. Traefik supporta router TCP e UDP sui propri entrypoint. Caddy richiede un altro plugin e quindi un'altra build personalizzata.
  • Un web server già dietro il proxy. Se il servizio è una classica applicazione PHP, allora uno stack LAMP su Ubuntu 24.04 include già Apache, mentre aggiungere un proxy davanti crea due punti in cui vengono impostati gli header e due punti in cui un URL può essere riscritto. Decidere quale componente termina TLS, quindi mantenere l'altro su HTTP non cifrato in ascolto sull'interfaccia di loopback.

La trappola del firewall che segue questa scelta

Lo scopo di un reverse proxy è lasciare aperte soltanto le porte 80 e 443. Docker annulla questo isolamento senza segnalarlo chiaramente. La pubblicazione di una porta con -p 8080:80 scrive una regola DNAT nella tabella nat. Questa regola viene valutata prima delle regole INPUT gestite da ufw. Di conseguenza, ufw deny 8080 non la blocca e l'applicazione viene esposta su Internet accanto al proxy configurato con tanta attenzione. Associa le porte pubblicate all'interfaccia loopback con 127.0.0.1:8080:80 oppure elimina completamente ports: e consenti al proxy di raggiungere il container tramite una rete Docker, come nell'esempio con Traefik riportato sopra. Il meccanismo e la soluzione sono descritti in perché le porte pubblicate da Docker bypassano ufw.

Esegui il test da una macchina diversa dal VPS, perché un controllo eseguito direttamente sul server ha sempre esito positivo:

curl --max-time 5 http://your.server.address:8080

Il risultato atteso è Connection refused oppure un timeout. Una risposta HTTP indica che l'applicazione è raggiungibile senza passare dal proxy. In tal caso, tutta la configurazione eseguita sopra è solo ornamentale.

Quale proxy scegliere?

Soprattutto siti statici, con una o due applicazioni: Caddy. HTTPS automatico elimina la principale attività ricorrente. La configurazione resta abbastanza breve da poter essere letta in una sola schermata, e un sito statico richiede una riga root e una riga file_server all'interno dello stesso blocco del sito. Il compromesso è una disponibilità minore di soluzioni pronte da copiare e incollare quando si verifica un problema insolito.

Un homelab basato su docker-compose a cui aggiungi continuamente nuovi servizi: Traefik. Oltre il terzo servizio, usare le label richiede meno lavoro che modificare un file centrale, e quando elimini un servizio elimini anche la relativa route. Metti in conto un pomeriggio per la prima configurazione, perché entrypoint, router, servizi e middleware sono tutti termini nuovi. Un errore di battitura in una label di solito si manifesta con un 404 restituito da Traefik, non con un errore di avvio; quindi leggi docker logs traefik per individuare l'errore di analisi prima di presumere che l'applicazione sia guasta.

Una configurazione Nginx esistente o qualsiasi requisito dell'elenco precedente: Nginx. Dispone già di una soluzione per la cache delle risposte e per i certificati client, e quasi tutte le guide di terze parti lo danno per scontato. Il compromesso è che i certificati e il supporto WebSocket devono essere configurati esplicitamente, invece di essere disponibili automaticamente.

Una regola vale indipendentemente dalla scelta. Esattamente un processo resta in ascolto sull'interfaccia pubblica, mentre tutto il resto resta in ascolto su loopback o su una rete Docker privata.

FAQ

Qual è il reverse proxy migliore per alcune applicazioni Docker su un unico VPS?

Per tre o quattro servizi che aggiungi solo occasionalmente, Traefik ripaga il tempo necessario per configurarlo, perché ogni applicazione include le proprie label di routing e non richiede modifiche a un file centrale. Se i servizi sono stabili e vuoi soprattutto non doverti occupare di HTTPS, Caddy richiede meno configurazione e offre meno possibilità di errore. Scegli Nginx se lo conosci già oppure se ti serve una funzione che gli altri due non offrono, come la memorizzazione nella cache delle risposte o un listener TCP diretto.

Caddy non richiede davvero alcuna configurazione dei certificati?

Nel caso normale, sì. Specificare un hostname pubblico come indirizzo del sito costituisce l'intera configurazione: Caddy richiede il certificato tramite ACME, serve il redirect dalla porta 80 e rinnova il certificato prima della scadenza. Devono comunque essere vere due condizioni. La porta 80 deve essere raggiungibile da Internet per la challenge HTTP-01 e il record DNS A o AAAA dell'hostname deve già puntare al VPS, perché la certification authority risolve il nome e si connette nuovamente al server.

Posso eseguire Nginx e Traefik sullo stesso VPS?

Non sulle stesse porte. Quello che si avvia per secondo non riesce a eseguire il bind e nginx restituisce bind() to 0.0.0.0:443 failed (98: Address already in use), mentre Traefik registra un errore di bind analogo e termina. Esegui un solo proxy sulle porte 80 e 443 e instrada tutto il resto tramite quel proxy. Se stai effettuando una migrazione, sposta gli hostname uno alla volta: lascia che il proxy front-end inoltri le richieste al proxy precedente su una porta di loopback finché non hai trasferito l'ultimo sito.

Perché le connessioni WebSocket si interrompono dopo 60 secondi dietro Nginx?

proxy_read_timeout ha un valore predefinito di 60 secondi e si applica al tunnel dopo il completamento dell'upgrade. Di conseguenza, una connessione senza traffico per un minuto viene chiusa dal proxy e non dall'applicazione. Aumenta il valore per quella location con proxy_read_timeout 3600s; oppure configura l'applicazione in modo che invii un frame ping ogni 30 secondi. Caddy e Traefik non chiudono le connessioni aggiornate inattive dopo un minuto, quindi la stessa applicazione può risultare stabile dietro questi proxy e instabile dietro Nginx.