SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-27

Traefik reverse proxy: 5 app con un solo Compose

Configura cinque app su un IP con Traefik v3 e Docker Compose: routing Host, TLS Let's Encrypt automatico e il problema di acme.json che blocca l'avvio.

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: web su :80 e websecure su :443.
  • I router associano una richiesta (Host(...)) a un servizio. I certificati vengono richiesti per ogni router tramite tls.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 la base dell'architettura. Traefik raggiunge un container solo se entrambi sono collegati a questa 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=3000

Prima docker compose up -d, quindi docker compose logs -f traefik. Ogni applicazione aggiuntiva è una copia del blocco gitea, con il proprio nome del router, il proprio Host() e la propria porta interna. 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 particolarmente 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.

providers.docker.network=proxy indica a Traefik quale rete utilizzare quando un container è collegato a più reti. Se lo ometti, Traefik potrebbe selezionare 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. Nota che nessun container applicativo pubblica porte: le pubblica soltanto Traefik.

Il redirect sull'entrypoint web converte le richieste in chiaro in una risposta 308 verso HTTPS. La porta 80 deve comunque restare aperta: è necessaria per la challenge HTTP di ACME e per gli utenti che digitano un nome host senza specificare il protocollo.

Il doppio $$ nell'hash di basic authentication è l'escaping 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=web

Entrambe le modalità funzionano. In entrambi i casi, il DNS pubblico del nome host deve puntare già al tuo VPS e l'autorità di certificazione deve poter 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 veniamo al 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 il file è leggibile dal gruppo o da tutti gli utenti, Traefik stampa una riga molto simile a questa e si arresta:

error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600

La soluzione corretta è quella indicata sopra: monta in bind la directory e lascia che sia Traefik a creare il file con i permessi corretti. Se hai creato acme.json con touch, la tua umask lo ha impostato a 644. Correggilo sull'host:

chmod 600 ./letsencrypt/acme.json
docker compose restart traefik

Esegui il backup di questa directory insieme ai volumi dell'applicazione. La sua perdita è gestibile: i certificati vengono emessi di nuovo, ma richiedere nuovamente cinque nomi host contemporaneamente può portarti a raggiungere i limiti di frequenza.

Usa la CA di staging durante i test. Decommenta la riga caserver, verifica che ogni route funzioni, quindi commenta la riga e rimuovi acme.json, in modo che i certificati per l'ambiente di produzione vengano richiesti da zero. Let's Encrypt in produzione consente cinque certificati duplicati alla settimana per uno stesso insieme 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 meno restrittivi.

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-auth

Una 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:
      - proxy

Rimuovi il volume del socket da Traefik e configura il provider per usare il proxy:

--providers.docker.endpoint=tcp://dockerproxy:2375

Traefik 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 enable

Le 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 vedrete realmente

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’etichetta è diverso da quello in un’altra (routers.gitea.rule e routers.gitea.entrypoints devono essere la stessa parola); oppure il nome host è racchiuso tra virgolette invece che tra backtick. Traefik v3 richiede i backtick all’interno dei matcher.

502 Bad Gateway. Un router ha trovato una corrispondenza, ma il backend non era raggiungibile. Quasi sempre il container non si trova nella rete proxy: controllare docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. L’altra possibilità è un valore loadbalancer.server.port errato: è stata indicata una porta pubblicata oppure l’applicazione è in ascolto su un’altra porta. Il log indica il tentativo: dial tcp 172.18.0.5:8080: connect: connection refused.

Il browser mostra un avviso e il certificato è stato emesso per TRAEFIK DEFAULT CERT. Non esiste alcun certificato per quel nome host e Traefik ha servito il proprio certificato autofirmato di fallback. 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.com

Il DNS non punta ancora al server. Correggere il record, attendere la scadenza del TTL e riavviare 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 del provider davanti al VPS, non ufw.

I certificati non vengono mai emessi e il DNS è gestito da Cloudflare con la nuvola arancione attiva. Cloudflare termina TLS sulla propria edge e TLS-ALPN-01 non può essere completata attraverso di essa. Impostare il record su DNS-only durante l’emissione oppure passare alla challenge DNS-01 con un token API. DNS-01 è anche l’unica challenge che consente di emettere certificati wildcard.

Ciclo di redirect. Un componente davanti a Traefik termina già TLS e inoltra traffico in chiaro verso :80; il redirect dell’entrypoint lo reindirizza nuovamente a HTTPS. Rimuovere 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 il controllo sull'ordine di avvio.

Fissa il tag di Traefik (traefik:v3.5, mai latest). L'aggiornamento dalla versione v2 alla v3 ha modificato la sintassi delle regole e i nomi dei provider; un latest non supervisionato ricarica senza problemi una configurazione che non è più in grado di interpretare. Esegui l'aggiornamento in modo pianificato: leggi le note sulla migrazione, modifica il tag, docker compose up -d traefik e monitora il log. Se usi ancora un tag v2, la guida alla migrazione di Traefik da v2 a v3 descrive ogni ridenominazione, la modalità di compatibilità e una procedura di rollback che conserva i certificati.

Esegui il backup di ./letsencrypt e del volume dati di ogni applicazione. Traefik non conserva altri dati di stato che non possano essere ricreati 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

Mi serve ancora certbot se uso Traefik?

No. Il resolver ACME di Traefik richiede e rinnova il certificato per ogni hostname che instrada e salva tutti i certificati 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 inutilmente i rate limit 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 quando è 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 i backtick all'interno del matcher, non le virgolette.

Qual è la differenza tra un 404 e un 502 in questo caso?

Un 404 indica che il routing 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 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.

Montare il socket Docker in sola lettura è sufficiente?

Il flag :ro rende di sola lettura il file del socket, non l'API esposta tramite quel socket; le richieste POST continuano a transitare attraverso di esso e l'accesso all'API Docker equivale all'accesso root sull'host. La configurazione più sicura è 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 ciascuna un singolo hostname e non possono generare un certificato wildcard. DNS-01 è inoltre la soluzione necessaria quando una CDN come Cloudflare gestisce la terminazione TLS davanti al VPS e le altre due challenge non vengono mai completate.