Traefik v3 Docker Compose: 5 app con un IP
Configura 5 app su un unico IP con Traefik v3 e Docker Compose. Guida su Host rule, TLS automatico e come evitare l'errore acme.json che blocca l'avvio.
Un IP, cinque app, una porta 443
Il VPS dispone di un unico indirizzo IPv4 pubblico e di una singola porta TCP 443. È necessario ospitare Gitea, una copia di staging della tua app, una dashboard interna, una pagina di stato e un ricevitore webhook — cinque hostname su un unico server. Un reverse proxy è il processo che gestisce le porte :80 e :443, legge l'header Host in ogni richiesta e la inoltra al container corretto. Traefik svolge questo compito e ottiene/rinnova un certificato per ogni hostname senza richiedere l'esecuzione manuale di certbot.
La differenza tra Traefik e un blocco nginx server {} risiede nell'origine della configurazione. Con nginx si modifica un file e si ricarica il servizio; la gestione del ciclo di vita dei certificati rimane un compito separato. Questo è il workflow utilizzato quando si emettono certificati Let's Encrypt con certbot su nginx, dove il timer di rinnovo è esterno al web server. Il provider Docker di Traefik monitora lo stream degli eventi Docker e legge le labels dai container: avvia un container con una label di regola Host() e questo diventa raggiungibile in un secondo; arrestalo e la rotta scompare. Questo è anche un potenziale problema. La configurazione memorizzata nelle labels è distribuita in cinque punti diversi; una label errata non produce errori espliciti — il container non viene semplicemente instradato e Traefik non segnala alcun errore.
I quattro sostantivi
- Entrypoints sono socket in ascolto. Ne definirai due:
websu:80ewebsecuresu:443. - Routers associano una richiesta (
Host(...)) a un servizio. I certificati vengono richiesti per ogni router tramitetls.certresolver. - Services rappresentano il backend: un container e la porta su cui è in ascolto all'interno della rete Docker.
- Middlewares si trovano tra il router e il servizio: basic auth, liste di IP consentiti, riscrittura degli header, redirect.
La configurazione statica (entrypoints, providers, ACME) viene passata tramite la riga di comando di Traefik o in traefik.yml; modificarla richiede il riavvio di Traefik. La configurazione dinamica (routers, services, middlewares) proviene dai label dei container ed è soggetta a hot-reload. Confondere i due tipi è la causa comune dell'errore "il mio flag non produce effetti".
Il file compose
Una rete Docker condivisa denominata proxy funge da infrastruttura principale. Traefik raggiunge un container solo se entrambi sono collegati alla stessa rete.
name: edge
networks:
proxy:
name: proxy
services:
traefik:
image: traefik:v3.5
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=3000docker compose up -d, quindi docker compose logs -f traefik. Ogni applicazione aggiuntiva è una copia del blocco gitea con il proprio nome router, il proprio Host() e la propria porta interna. Un installazione di Nextcloud in Docker con TLS e backup si integra nello stesso modo: rimuovere le porte pubblicate, collegarlo a proxy e lasciare che le label del router gestiscano l'hostname e il certificato.
Cinque dettagli sono fondamentali.
exposedByDefault=false rende un container invisibile a Traefik finché non contiene traefik.enable=true. Se viene omessa, ogni container avviato — inclusi i container temporanei postgres usati per i test — avrà una rotta generata automaticamente.
providers.docker.network=proxy indica a Traefik quale rete utilizzare quando un container è collegato a più reti. Se omessa, Traefik potrebbe selezionare l'IP errato del container, causando un errore 502 che sembra un guasto dell'applicazione.
loadbalancer.server.port=3000 è la porta interna al container; Gitea ascolta sulla porta 3000. Si noti che nessun container applicativo pubblica porte: solo Traefik lo fa.
Il redirect sull'entrypoint web converte le richieste in chiaro in un redirect 308 verso HTTPS. La porta 80 rimane aperta: è necessaria per la challenge HTTP di ACME e per gli utenti che digitano l'hostname senza protocollo.
Il raddoppio di $$ nell'hash basic-auth è dovuto all'escaping di Compose, non è un errore. Generalo con htpasswd -nbB admin 'your-password' (pacchetto apache2-utils), quindi raddoppia ogni $.
Il certificato e l'insidia 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 il TLS handshake. L'alternativa è HTTP-01, sulla porta 80 — sostituisci la riga tlschallenge nella lista command: di Traefik con queste due:
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=webEntrambi i metodi sono validi. Entrambi richiedono che il DNS pubblico per l'hostname punti già al VPS — l'autorità di certificazione risolve il nome e si connette dall'esterno. Crea prima il record A (e AAAA), conferma con dig +short git.example.com, quindi avvia Traefik.
Ecco l'insidia che fa perdere un'intera serata agli utenti. Traefik conserva la chiave dell'account ACME e ogni certificato emesso in un unico file acme.json. Se il file è leggibile dal gruppo o da tutti gli utenti, Traefik stampa una riga simile a questa e si arresta:
error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600La soluzione corretta è quella indicata sopra: usa un bind-mount per la directory e lascia che sia Traefik a creare il file con i permessi corretti. Se hai creato acme.json con touch, la umask ha impostato i permessi a 644. Ripara il file sull'host:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikEsegui il backup di quella directory insieme ai volumi della tua app. La perdita del file è gestibile — i certificati vengono riemessi — ma la riemissione simultanea di cinque hostname causa il superamento dei rate limits.
Usa la staging CA durante la fase di test. Decommenta la riga caserver, configura correttamente ogni route, quindi commenta nuovamente la riga e cancella acme.json per richiedere nuovi certificati di produzione. Let's Encrypt in produzione permette cinque certificati duplicati a settimana per lo stesso set di hostname, e limita le richieste in caso di validazioni fallite ripetutamente per lo stesso nome. Lo staging emette certificati non fidati — il browser mostra un avviso, e quell'avviso conferma che il processo ha funzionato — con limiti molto più permissivi.
La dashboard è una superficie di controllo, non una demo
La maggior parte delle guide rapide configura --api.insecure=true, che espone la dashboard sulla porta 8080 senza autenticazione. Questo espone la topologia di routing, gli hostname, i nomi dei middleware e le porte backend a chiunque esegua una scansione su un server con IP pubblico.
Le etichette sul servizio traefik sopra rappresentano 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 alle API integrate di Traefik. Per una sicurezza maggiore, applica una allow-list di IP da sinistra a destra. Se l'indirizzo del tuo ufficio è dinamico, imposta l'intervallo sulla subnet fornita da una WireGuard VPN self-hosted sullo stesso VPS e accedi alla dashboard solo tramite il tunnel:
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-authIl socket di Docker è root
/var/run/docker.sock è un'API in grado di creare un container che monta / dall'host. L'accesso a questa API equivale a avere privilegi di root sulla macchina; Traefik ne ha bisogno per leggere le labels.
Mantieni il :ro nel mount, ma considera le conseguenze: rende il file del socket in sola lettura. Non impedisce le richieste POST alla Docker API tramite il socket stesso. La vera mitigazione consiste nel non fornire mai il socket direttamente a Traefik, inserendo un proxy di filtraggio tra i due:
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 punta il provider verso il proxy:
--providers.docker.endpoint=tcp://dockerproxy:2375Traefik mantiene l'accesso in sola lettura a container e network, perdendo la capacità di creare qualsiasi risorsa.
Firewall, porte e la regola che tutti sbagliano
Due porte aperte, più 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 le proprie regole iptables, che vengono valutate prima delle catene di ufw. Un container avviato con ports: ["3000:3000"] è raggiungibile da internet anche se ufw è impostato su deny. La difesa è strutturale, non dipende dalla configurazione del firewall: pubblica le porte solo da Traefik e assegna a tutti gli altri container networks: [proxy] e nient'altro. Se un servizio deve necessariamente raggiungere l'host, vincolalo al loopback — "127.0.0.1:3000:3000".
Troubleshooting: errori effettivi
404 page not found, servito da Traefik. Nessun router corrisponde. In ordine di probabilità: il container non ha traefik.enable=true (con exposedByDefault=false impostato); la rule Host() non corrisponde al nome digitato; il nome del router in un label è diverso dal nome del router in un altro (routers.gitea.rule e routers.gitea.entrypoints devono essere la stessa parola); oppure il hostname è racchiuso tra virgolette invece che tra backtick. Traefik v3 richiede i backtick all'interno dei matcher.
502 Bad Gateway. Un router ha corrisposto ma il backend non è raggiungibile. Quasi sempre il container non si trova sulla rete proxy — controllare docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. L'altra causa possibile è un loadbalancer.server.port errato: è stato indicato una porta pubblicata o l'app ascolta su un'altra porta. Il log indica il tentativo: dial tcp 172.18.0.5:8080: connect: connection refused.
Il browser avvisa, e il certificato è rilasciato a TRAEFIK DEFAULT CERT. Non esiste un certificato per quell'hostname e Traefik ha servito il proprio placeholder self-signed. Leggere 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. Correggere il record, attendere la scadenza del TTL, riavviare Traefik.
Invalid response from http://git.example.com/.well-known/acme-challenge/... sull'HTTP challenge: la porta 80 non raggiunge Traefik dall'esterno — solitamente si tratta di un firewall a livello di provider davanti al VPS, non ufw.
I certificati non vengono mai emessi e il DNS è su Cloudflare con la nuvola arancione attiva. Cloudflare termina il TLS al suo edge e il protocollo TLS-ALPN-01 non può completarsi attraverso di esso. Impostare il record su DNS-only durante l'emissione, oppure passare al challenge DNS-01 tramite un API token. DNS-01 è l'unico challenge che emette certificati wildcard.
Redirect loop. Un elemento davanti a Traefik termina già il TLS e inoltra testo in chiaro sulla porta :80; il redirect dell'entrypoint lo rimanda in HTTPS. Rimuovere uno dei due redirect.
Keeping it running
L'unit di Docker deve essere abilitata al boot (systemctl is-enabled docker) e restart: unless-stopped ripristina lo stack dopo un riavvio. Per un controllo esplicito, un piccolo unit di systemd che esegue docker compose -f /srv/edge/compose.yml up -d con RemainAfterExit=yes fornisce systemctl status edge e controllo dell'ordinamento.
Fissa il tag di Traefik (traefik:v3.5, mai latest). L'upgrade da v2 a v3 ha modificato la sintassi delle regole e i nomi dei provider; un latest non supervisionato ricaricherà una configurazione che non è più in grado di interpretare. Esegui l'upgrade deliberatamente: leggi le note di migrazione, aggiorna il tag, docker compose up -d traefik e monitora il log. Se utilizzi ancora un tag v2, la guida alla migrazione da Traefik v2 a v3 illustra ogni rinomina, la modalità di compatibilità e un rollback che preserva i certificati.
Esegui il backup di ./letsencrypt e del volume dati di ogni app. Traefik non mantiene altri stati che non siano riconfigurabili tramite il file compose.
Cosa fallisce su larga scala
Il primo limite non è il throughput, ma il singolo server: un'istanza di Traefik su un VPS rappresenta un single point of failure per cinque app, e acme.json utilizza il file storage — due istanze di Traefik che scrivono sullo stesso file causeranno la corruzione dei dati. Scalare significa spostare lo storage dei certificati fuori dai file, oppure terminare il TLS altrove.
Il secondo limite riguarda le connessioni a lunga durata. Server-sent events, upload di grandi dimensioni e client lenti possono superare i timeout di risposta dell'entrypoint; --entryPoints.websecure.transport.respondingTimeouts.readTimeout e i suoi sibling writeTimeout e idleTimeout sono i parametri di configurazione. I WebSocket vengono gestiti tramite pass-through senza configurazioni aggiuntive.
Il terzo limite è il disco. --accesslog=true scrive su stdout, e il driver json-file di Docker conserva i log indefinitamente se non viene limitato. Imposta logging.options.max-size sul servizio Traefik, oppure scrivi l'access log su un file e applica la rotazione.
Nessuno di questi problemi richiede un orchestratore. È necessario un server sotto il proprio controllo, con un IP reale e le porte 80 e 443 aperte verso l'esterno — un singolo VPS di piccole dimensioni è l'unica dipendenza necessaria.
FAQ
Ho ancora bisogno di certbot se uso Traefik?
No. L'ACME resolver di Traefik richiede e rinnova il certificato per ogni hostname instradato e salva tutto in acme.json. Certbot è lo strumento corretto quando nginx o un altro server gestisce direttamente il TLS; eseguire entrambi per gli stessi hostname consuma inutilmente i limiti di rate limit di Let's Encrypt.
Perché il mio container restituisce 404 tramite Traefik?
Un errore 404 restituito da Traefik indica che nessun router ha corrisposto alla richiesta. Verifica che il container abbia traefik.enable=true (obbligatorio dopo l'impostazione di exposedByDefault=false), che il valore Host() corrisponda al nome digitato e che il nome del router sia identico in ogni label dell'app. Traefik v3 richiede l'uso di backtick all'interno del matcher, non virgolette.
Qual è la differenza tra un 404 e un 502 in questo contesto?
Un 404 significa che l'instradamento non è avvenuto; un 502 significa che un router ha corrisposto alla richiesta ma il backend ha rifiutato la connessione. I comuni casi di errore 502 riguardano un container non collegato alla rete proxy o un loadbalancer.server.port che punta a una porta pubblicata invece della porta su cui l'app ascolta all'interno del container. L'access log indica l'indirizzo esatto contattato da Traefik.
Montare il Docker socket in sola lettura è sufficiente?
Il flag :ro rende il file del socket in sola lettura, non l'API sottostante — le richieste POST passano comunque attraverso di esso e l'accesso all'API di Docker equivale ad avere i privilegi di root sull'host. La configurazione più sicura è il container docker-socket-proxy mostrato sopra, che espone a Traefik solo la lettura di container e reti e blocca completamente le operazioni di scrittura.
Traefik può emettere un certificato wildcard?
Solo tramite la challenge DNS-01, utilizzando un token API del proprio provider DNS. Le challenge TLS-ALPN-01 e HTTP-01 validano un singolo hostname e non possono generare un wildcard. La challenge DNS-01 è la soluzione necessaria quando un CDN come Cloudflare termina il TLS davanti al VPS e le altre due challenge non possono completarsi.