Traefik reverse proxy: 5 app con Docker Compose
Configura 5 app su un solo IP con Traefik v3 e Docker Compose: routing con Host, TLS Let's Encrypt automatico e il problema di acme.json che blocca il rilascio.
Un indirizzo IP, cinque applicazioni, una porta 443
Il tuo VPS ha un unico indirizzo IPv4 pubblico e un'unica porta TCP 443. Vuoi eseguire Gitea, una copia di staging della tua applicazione, una dashboard interna, una pagina di stato e un ricevitore di webhook: cinque hostname su un unico server. Un reverse proxy è il processo che gestisce :80 e :443, legge l'header Host di ogni richiesta e la inoltra al container corretto. Traefik svolge questo compito e ottiene e rinnova un certificato per ogni hostname, senza dover mai eseguire manualmente certbot. Anche Nginx e Caddy potrebbero gestire senza problemi gli stessi cinque hostname. Se la scelta non è ancora definitiva, conviene confrontare i tre proxy in base alla gestione dei certificati e al costo di configurazione per applicazione prima di collegare tutto a uno di essi.
Ciò che distingue Traefik da un blocco server {} di nginx è l'origine della configurazione. Con nginx modifichi un file ed esegui un reload, mentre il ciclo di vita del certificato resta un'attività separata. È il flusso che segui quando rilasci certificati Let's Encrypt con certbot su nginx, con un timer per il rinnovo che risiede completamente al di fuori del web server. Il provider Docker di Traefik monitora il flusso degli eventi Docker e legge le label dai container: avvia un container con una label contenente una regola Host() e il container diventa raggiungibile entro un secondo; arrestalo e la route scompare. Questo è anche il rischio principale. La configurazione memorizzata nelle label si trova contemporaneamente in cinque punti e una label errata non produce errori: il container semplicemente non viene instradato e Traefik non segnala nulla.
I quattro concetti
- Entrypoints sono socket in ascolto. Ne definirai due:
websu:80ewebsecuresu:443. - I router associano una richiesta (
Host(...)) a un servizio. I certificati vengono richiesti per ogni router tramitetls.certresolver. - I service sono il backend: un container e la porta su cui è in ascolto all'interno della rete Docker.
- I middleware si collocano tra router e servizio: autenticazione di base, allow-list di indirizzi IP, riscrittura degli header e redirect.
Questi quattro concetti sono i nomi che Traefik assegna a operazioni che altrimenti dovresti configurare manualmente: il router è un server_name, il servizio è una destinazione proxy_pass e i middleware corrispondono alle direttive per header e autenticazione che imposteresti manualmente quando crei riga per riga un server block di nginx come reverse proxy.
La configurazione statica (entrypoints, provider, ACME) viene passata sulla riga di comando di Traefik o in traefik.yml; per modificarla è necessario riavviare Traefik. La configurazione dinamica (router, servizi, middleware) proviene dalle label dei container e viene ricaricata automaticamente. Confondere i due tipi di configurazione è la causa più comune del problema "il mio flag non ha effetto".
Il file Compose
Una rete Docker condivisa denominata proxy costituisce l'elemento centrale. Traefik raggiunge un container solo se entrambi sono collegati a questa rete.
name: edge
networks:
proxy:
name: proxy
services:
traefik:
image: traefik:v3.7
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedByDefault=false
- --providers.docker.network=proxy
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
- --entryPoints.web.http.redirections.entryPoint.to=websecure
- --entryPoints.web.http.redirections.entryPoint.scheme=https
- --certificatesresolvers.le.acme.email=you@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.tlschallenge=true
# while you iterate, point at staging so a mistake costs nothing:
# - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
- --api.dashboard=true
- --log.level=INFO
- --accesslog=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
- traefik.http.routers.dashboard.entrypoints=websecure
- traefik.http.routers.dashboard.tls.certresolver=le
- traefik.http.routers.dashboard.service=api@internal
- traefik.http.routers.dashboard.middlewares=dashboard-auth
- traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$REPLACE$$THIS
gitea:
image: gitea/gitea:1 # major-only pin keeps this demo copy-pasteable; pin an exact release in production
restart: unless-stopped
volumes:
- ./gitea:/data
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.gitea.rule=Host(`git.example.com`)
- traefik.http.routers.gitea.entrypoints=websecure
- traefik.http.routers.gitea.tls.certresolver=le
- traefik.http.services.gitea.loadbalancer.server.port=3000Prima docker compose up -d, quindi docker compose logs -f traefik. Ogni applicazione aggiuntiva è una copia del blocco gitea, con un nome del router, un Host() e una porta interna propri. Anche una installazione di Nextcloud in Docker con TLS e backup si integra allo stesso modo: rimuovi le porte pubblicate, collegala a proxy e lascia che le label del router gestiscano il nome host e il certificato.
Cinque dettagli sono importanti.
exposedByDefault=false rende un container invisibile a Traefik finché non contiene traefik.enable=true. Se lo ometti, ogni container avviato, incluso il container temporaneo postgres usato per una verifica, riceve una route generata.
providers.docker.network=proxy indica a Traefik quale rete utilizzare quando un container è collegato a più reti. Se lo ometti, Traefik può scegliere l'indirizzo IP errato del container. Il risultato è un errore 502 che può sembrare un problema dell'applicazione.
loadbalancer.server.port=3000 è la porta interna al container; Gitea è in ascolto sulla porta 3000. Nessun container applicativo pubblica una porta: lo fa soltanto Traefik.
Il redirect sull'entrypoint web è permanente per impostazione predefinita. Una richiesta GET in chiaro, come quella inviata da un browser, riceve quindi un redirect 301 verso HTTPS. Traefik restituisce 308 solo per gli altri metodi, ad esempio POST, così il metodo viene mantenuto durante il passaggio. La porta 80 rimane comunque aperta: è necessaria per la challenge HTTP di ACME e per gli utenti che digitano un nome host senza schema.
Il doppio $$ nell'hash di basic auth è un'operazione di escape di Compose, non un errore di battitura. Generalo con htpasswd -nbB admin 'your-password' (pacchetto apache2-utils), quindi raddoppia ogni $.
Il certificato e il problema di acme.json
tlschallenge=true seleziona TLS-ALPN-01: Let's Encrypt si connette al server sulla porta 443 e Traefik risponde alla challenge durante l'handshake TLS. L'alternativa è HTTP-01, sulla porta 80. Sostituisci la riga tlschallenge nell'elenco command: di Traefik con queste due:
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=webEntrambe le modalità funzionano. In tutti e due i casi, il DNS pubblico del nome host deve puntare già al tuo VPS, in modo che l'autorità di certificazione possa risolvere il nome e connettersi dall'esterno. Crea prima il record A (e AAAA), verifica con dig +short git.example.com, quindi avvia Traefik.
Ora considera il problema che può farti perdere un'intera serata. Traefik conserva la chiave dell'account ACME e tutti i certificati emessi in un unico acme.json. Se quel file è leggibile dal gruppo o da tutti gli utenti, Traefik registra una riga molto simile a questa e disabilita il resolver. Il servizio continua a funzionare e a instradare il traffico, ma non viene mai richiesto alcun certificato. Di conseguenza, ogni nome host riceve TRAEFIK DEFAULT CERT:
ERR The ACME resolve is skipped from the resolvers list error="unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600" resolver=leLa soluzione corretta è quella mostrata sopra: monta in bind la directory e lascia che sia Traefik a creare il file con i permessi appropriati. Se hai creato acme.json con touch, la umask lo ha impostato a 644. Correggilo sull'host:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikInserisci anche questa directory nel backup insieme ai volumi dell'applicazione. Se la perdi una volta, il problema è recuperabile: i certificati vengono emessi di nuovo. Tuttavia, eliminarla a ogni nuova distribuzione ti porta a raggiungere il limite di cinque certificati per ogni set esatto di nomi host nell'arco di 7 giorni.
Usa la CA di staging durante le prove. Rimuovi il commento dalla riga caserver, verifica che tutte le route funzionino, quindi aggiungi nuovamente il commento e elimina acme.json, in modo che i certificati di produzione vengano richiesti da zero. Let's Encrypt in produzione consente cinque certificati duplicati alla settimana per uno stesso set di nomi host e limita le convalide fallite ripetute per lo stesso nome. L'ambiente di staging emette certificati non attendibili: il browser mostra un avviso. Questo avviso indica che la procedura ha funzionato e i limiti sono molto più permissivi.
La dashboard è una superficie di controllo, non una demo
La maggior parte delle guide rapide imposta --api.insecure=true, che espone la dashboard sulla porta 8080 senza autenticazione. Su un server con un IP pubblico, questa configurazione rende disponibili a chiunque esegua una scansione la topologia di routing, i nomi host, i nomi dei middleware e le porte dei backend.
Le label del servizio traefik mostrato sopra sono l'alternativa: la dashboard viene instradata come qualsiasi altra applicazione, su un hostname reale, tramite TLS e dietro basicauth. service=api@internal collega il router all'API integrata di Traefik. È possibile aumentare ulteriormente la protezione concatenando un elenco di indirizzi IP consentiti, applicato da sinistra a destra. Se l'indirizzo dell'ufficio è dinamico, imposta l'intervallo sulla subnet assegnata da una VPN WireGuard self-hosted sullo stesso VPS e accedi alla dashboard soltanto tramite il tunnel:
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-authUna password condivisa basicauth non è più una soluzione sostenibile quando cinque applicazioni devono avere account separati. Lo stesso slot del middleware può invece usare un forwardauth che delega la decisione a Authentik, un server self-hosted per il single sign-on, così la dashboard e ogni route associata sono protette da un unico accesso, revocabile da un solo punto.
Il socket Docker equivale a root
/var/run/docker.sock è un'API che può creare un container con un mount di / dall'host. L'accesso al socket equivale ad avere privilegi root sulla macchina. Traefik ne ha bisogno per leggere le label.
Mantieni :ro nel mount, ma chiarisci cosa garantisce: rende di sola lettura il file del socket. Non impedisce che le richieste POST all'API Docker lo attraversino. La mitigazione effettiva consiste nel non fornire mai il socket a Traefik e nell'inserire un proxy con filtro tra Traefik e Docker:
dockerproxy:
image: tecnativa/docker-socket-proxy # pin the current tag
restart: unless-stopped
environment:
CONTAINERS: 1
NETWORKS: 1
POST: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxyRimuovi il volume del socket da Traefik e configura il provider per usare il proxy:
--providers.docker.endpoint=tcp://dockerproxy:2375Traefik mantiene l'accesso in lettura a container e reti, ma non può più creare alcun oggetto.
Firewall, porte e la regola che tutti applicano in modo errato
Due porte aperte, oltre a SSH:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableLe porte pubblicate da Docker bypassano ufw. Docker inserisce regole iptables proprie, valutate prima delle chain di ufw. Di conseguenza, un container avviato con ports: ["3000:3000"] è raggiungibile da Internet anche se ufw applica una regola deny alla stessa porta. La difesa è strutturale, non dipende dalla configurazione del firewall: pubblicare le porte soltanto da Traefik e assegnare a ogni altro container networks: [proxy], senza altre porte. Se un servizio deve realmente raggiungere l'host, associarlo al loopback, "127.0.0.1:3000:3000".
Risoluzione dei problemi: errori che incontrerai davvero
404 page not found, servito da Traefik. Nessun router ha trovato una corrispondenza. In ordine di probabilità: al container manca traefik.enable=true (con exposedByDefault=false impostato); la regola Host() non corrisponde al nome digitato; il nome del router in un label è diverso da quello in un altro (routers.gitea.rule e routers.gitea.entrypoints devono essere la stessa parola); oppure hai racchiuso l'hostname tra apici singoli. Traefik v3 accetta i backtick o le virgolette doppie con escape (\") all'interno dei matcher e rifiuta gli apici singoli.
502 Bad Gateway. Un router ha trovato una corrispondenza, ma il backend non era raggiungibile. Quasi sempre il container non si trova sulla rete proxy; controlla docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. L'altra possibilità è un loadbalancer.server.port errato: hai indicato una porta pubblicata oppure l'applicazione è in ascolto altrove. Il log indica il tentativo effettuato: dial tcp 172.18.0.5:8080: connect: connection refused.
Il browser mostra un avviso e il certificato è intestato a TRAEFIK DEFAULT CERT. Non esiste alcun certificato per quell'hostname e Traefik ha servito il proprio certificato autofirmato segnaposto. Leggi le righe ACME:
unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.comIl DNS non punta ancora al server. Correggi il record, attendi la scadenza del TTL e riavvia Traefik.
Invalid response from http://git.example.com/.well-known/acme-challenge/... durante la challenge HTTP: la porta 80 non raggiunge Traefik dall'esterno. Di solito la causa è un firewall a livello del provider davanti al VPS, non ufw.
I certificati non vengono mai emessi e il DNS è su Cloudflare con la nuvola arancione attiva. Cloudflare termina TLS sul proprio edge e TLS-ALPN-01 non può essere completata attraverso di esso. Imposta il record su DNS-only durante l'emissione oppure passa alla challenge DNS-01 con un token API. DNS-01 è anche l'unica challenge che emette certificati wildcard.
Loop di redirect. Un componente davanti a Traefik termina già TLS e inoltra traffico non cifrato alla porta :80; il redirect dell'entrypoint lo rimanda a HTTPS. Rimuovi uno dei due redirect.
Mantenere il servizio operativo
L'unità Docker deve essere abilitata all'avvio (systemctl is-enabled docker) e restart: unless-stopped ripristina lo stack dopo un riavvio. Per un controllo esplicito, una piccola unità systemd che esegue docker compose -f /srv/edge/compose.yml up -d con RemainAfterExit=yes offre systemctl status edge e controllo sull'ordine di avvio.
Blocca il tag di Traefik (traefik:v3.7, mai latest) e usa un branch che riceva ancora release. Al 2026-10-12 Traefik indica v3.7 come l'unico branch v3 ancora supportato. v3.5 ha pubblicato l'ultima release, v3.5.6, il 7 novembre 2025, e il relativo provider Docker imposta staticamente la versione API 1.24, che Docker Engine 29 e versioni successive rifiutano con client version 1.24 is too old; di conseguenza nessun container viene instradato. Traefik 3.6.1 ha aggiunto la negoziazione della versione dell'API Docker, per questo il file compose precedente usa v3.7. L'aggiornamento da v2 a v3 ha modificato la sintassi delle regole e i nomi dei provider; un latest non supervisionato ricarica senza problemi una configurazione che non comprende più. Esegui l'aggiornamento in modo controllato: leggi le note di migrazione, aggiorna il tag, docker compose up -d traefik e monitora il log. Se usi ancora un tag v2, la guida alla migrazione da Traefik v2 a v3 descrive ogni rinomina, la modalità di compatibilità e un rollback che conserva i certificati.
Esegui il backup di ./letsencrypt e del volume dati di ogni applicazione. Traefik non conserva altro stato che non possa essere ricreato dal file compose.
Cosa si rompe quando si scala
Il primo limite non è il throughput, ma il singolo server: un'istanza di Traefik su un solo VPS è un singolo punto di errore per cinque applicazioni e acme.json usa l'archiviazione in file flat; due istanze di Traefik che vi scrivono possono corromperlo. Per scalare orizzontalmente è necessario spostare l'archiviazione dei certificati fuori da un file oppure terminare TLS altrove.
Il secondo limite riguarda le connessioni di lunga durata. Gli eventi inviati dal server, gli upload di grandi dimensioni e i client lenti possono raggiungere i timeout di risposta dell'entrypoint; --entryPoints.websecure.transport.respondingTimeouts.readTimeout e i relativi writeTimeout e idleTimeout sono i parametri da configurare. WebSocket funzionano senza configurazione aggiuntiva.
Il terzo limite è il disco. --accesslog=true scrive su stdout e il driver json-file di Docker conserva questi dati indefinitamente, a meno che non venga imposto un limite. Imposta logging.options.max-size sul servizio Traefik oppure scrivi l'access log in un file e ruotalo.
Nulla di tutto questo richiede un orchestratore. Serve però un server sotto il tuo controllo, con un IP pubblico reale e le porte 80 e 443 aperte verso Internet. Un singolo VPS di piccole dimensioni è l'intero insieme delle dipendenze.
FAQ
Devo usare certbot anche se eseguo Traefik?
No. Il resolver ACME di Traefik richiede e rinnova il certificato per ogni hostname che instrada e memorizza tutto in acme.json. Certbot resta lo strumento corretto quando nginx o un altro server gestisce direttamente la terminazione TLS; eseguire entrambi sugli stessi hostname consuma soltanto i limiti di frequenza di Let's Encrypt.
Perché il mio container restituisce 404 tramite Traefik?
Un 404 restituito da Traefik indica che nessun router ha associato la richiesta. Verifica che il container contenga traefik.enable=true (obbligatorio dopo aver impostato exposedByDefault=false), che il valore di Host() corrisponda al nome digitato e che il nome del router sia identico in tutte le label dell'applicazione. Traefik v3 richiede inoltre backtick o virgolette doppie con escape all'interno del matcher; le virgolette singole vengono rifiutate.
Qual è la differenza tra un 404 e un 502 in questo caso?
Un 404 indica che l'instradamento non è mai avvenuto; un 502 indica che un router ha associato la richiesta, ma il backend ha rifiutato la connessione. Le cause più comuni di un 502 sono un container non collegato alla rete proxy e un valore loadbalancer.server.port che punta a una porta pubblicata invece che alla porta su cui l'applicazione è in ascolto all'interno del container. Il log degli accessi indica l'indirizzo esatto contattato da Traefik.
È sufficiente montare il socket Docker in sola lettura?
Il flag :ro rende di sola lettura il file del socket, non l'API esposta tramite quel socket; le richieste POST continuano a essere trasmesse e l'accesso all'API Docker equivale all'accesso root sull'host. L'assetto più sicuro è il container docker-socket-proxy mostrato sopra, che espone a Traefik soltanto le operazioni di lettura su container e reti e blocca completamente le operazioni di scrittura.
Traefik può emettere un certificato wildcard?
Solo tramite la challenge DNS-01, usando un token API del provider DNS. TLS-ALPN-01 e HTTP-01 convalidano un singolo hostname e non possono produrre un certificato wildcard. DNS-01 è inoltre la soluzione quando una CDN come Cloudflare gestisce la terminazione TLS davanti al VPS e le altre due challenge non vengono mai completate.