Docker e VPN: perché le porte dei container spariscono
Con Gluetun e network_mode: service, le porte pubblicate e i nomi Docker del container collegato spariscono. Ecco l'errore e il compose corretto.
Perché le porte scompaiono quando si instradano i container Docker attraverso una VPN
Per instradare i container Docker attraverso una VPN, si assegna il tunnel a un container e si collegano gli altri al suo namespace di rete con network_mode: "service:gluetun". È questo collegamento a creare problemi. Il container collegato non dispone più di una rete propria, quindi perde le porte pubblicate e il nome del servizio Docker. Pubblicare le porte sul container VPN e raggiungere l'applicazione dagli altri container tramite il nome del container VPN.
Se si lascia un blocco ports: sul container collegato, Docker ne rifiuta completamente la creazione:
Error response from daemon: conflicting options: port publishing and the container type network modeLo strumento utilizzato in questo esempio è Gluetun, un container che si connette a un provider VPN commerciale (rete privata virtuale) tramite WireGuard o OpenVPN e include un firewall proprio. La release v3.41.3 è quella corrente ad agosto 2026. Gli esempi usano Mullvad con WireGuard, quindi servono un account e una chiave forniti dal provider. Se si preferisce terminare il tunnel su hardware di propria gestione, la configurazione di un server WireGuard su un VPS crea l'altra estremità, mentre wg-easy in Docker aggiunge un'interfaccia web.
Cosa fa realmente network_mode: "service:gluetun"
Ogni container Docker normalmente ottiene un proprio network namespace: interfacce, tabella di routing, regole del firewall e socket in ascolto propri. La modalità service: salta questo passaggio e avvia il container all'interno del namespace di gluetun. Un solo namespace corrisponde a un solo indirizzo IP e questo comporta sei conseguenze.
- L'applicazione non ha un proprio indirizzo. Usa l'indirizzo di gluetun.
- L'applicazione non è collegata ad alcuna rete Docker, quindi il suo nome di servizio non viene mai registrato e non può essere risolto. Gli altri container devono usare
gluetun. - I container all'interno del namespace comunicano tra loro tramite
localhost. - Due container nello stesso namespace non possono mettersi in ascolto sulla stessa porta. La documentazione di Gluetun è esplicita: non esiste una soluzione alternativa.
- Le capability appartengono a un container, non a un namespace. Gluetun dispone di
NET_ADMINe/dev/net/tunperché crea l'interfaccia del tunnel. Il container collegato non le eredita. - Compose rifiuta qualsiasi file in cui un servizio imposta sia
network_modesianetworks. Collega gluetun alle tue reti e l'applicazione utilizzerà la stessa configurazione di rete.
Il riavvio di gluetun disconnette tutto ciò che vi è collegato. Questo comportamento è documentato e spiega perché gluetun riavvia il processo VPN all'interno del container invece di terminare quando la connessione non riesce. Dopo aver riavviato o ricreato manualmente gluetun, riavvia i container collegati.
Il file compose funzionante
services:
gluetun:
image: qmcgaw/gluetun:v3
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- SERVER_CITIES=Amsterdam
- TZ=Europe/Amsterdam
env_file:
- ./gluetun.env
volumes:
- ./gluetun:/gluetun
ports:
- 127.0.0.1:8080:8080/tcp
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
network_mode: "service:gluetun"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- WEBUI_PORT=8080
volumes:
- ./qbittorrent:/config
- ./downloads:/downloads
depends_on:
gluetun:
condition: service_healthy
restart: unless-stoppedIl tag :v3 è l'ultima release stabile della serie v3. Il tag :latest punta all'ultimo commit del branch master, cioè al ramo di sviluppo; quindi fissa :v3 su una macchina sulla quale non vuoi fare attività di debug di martedì.
WEBUI_PORT=8080 deve corrispondere alla porta pubblicata, perché qBittorrent si mette in ascolto nello spazio dei nomi di gluetun e la regola di pubblicazione inoltra il traffico dell'host alla porta 8080 in quello spazio dei nomi. Se modifichi un numero senza modificare anche l'altro, la porta non risponde. 127.0.0.1:8080:8080 mantiene l'interfaccia web sull'indirizzo di loopback dell'host. Un semplice 8080:8080 pubblica la porta su tutte le interfacce e crea una propria regola firewall: è così che le porte pubblicate da Docker aggirano direttamente ufw.
Avvia lo stack, quindi controllalo in questo ordine:
docker compose up -d
docker compose ps
docker compose logs gluetun | tail -30docker compose ps dovrebbe mostrare gluetun come healthy e qbittorrent come running. Verifica quindi l'indirizzo di uscita dall'interno dello spazio dei nomi. Questo controllo determina l'esito di tutte le verifiche successive:
docker run --rm --network=container:gluetun alpine:3.22 sh -c "apk add wget && wget -qO- https://ipinfo.io"Il campo ip di quel JSON dovrebbe contenere l'indirizzo del provider VPN. Se contiene l'indirizzo del server, l'applicazione non sta usando il tunnel e tutto ciò che segue non funzionerà come descritto.
Tieni le chiavi fuori dal file compose
gluetun.env contiene le credenziali e non viene incluso in git:
WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32Entrambi i valori provengono da un file di configurazione WireGuard generato nell'area account del provider. Imposta il file con la modalità 600. È importante essere chiari sul vantaggio effettivo: la chiave resta fuori dal repository, ma docker inspect gluetun stampa comunque tutte le variabili d'ambiente a chiunque possa raggiungere il socket Docker. File d'ambiente e secret in Docker Compose illustra opzioni più sicure.
Come comunica un container esterno al tunnel con uno interno
La comunicazione funziona in entrambe le direzioni e in ciascun caso usa un nome diverso. I due container devono appartenere a una rete Docker condivisa, che in questo caso è la rete di gluetun, perché il container collegato non dispone di una rete propria. Come sono collegate le reti Docker Compose descrive i valori predefiniti.
Per comunicare dall'esterno verso l'interno, usa il nome di gluetun e la porta su cui l'applicazione è in ascolto. Un container reverse proxy raggiunge l'interfaccia web di qBittorrent all'indirizzo gluetun:8080. Non serve alcuna voce ports:, perché il traffico tra container resta sulla rete Docker e non passa mai per una porta dell'host.
Per comunicare dall'interno verso l'esterno, usa il nome del servizio dell'altro container, ad esempio postgres:5432. Dalla versione v3.41, Gluetun risolve i nomi degli altri container all'interno del proprio namespace. Se un nome non viene risolto, usa quindi quella versione o una successiva.
Il firewall di Gluetun decide chi può aprire una connessione verso di esso. Il traffico proveniente dalla rete Docker di gluetun è consentito. Un client su una subnet diversa, un laptop della LAN o un container su una rete bridge separata viene bloccato finché non autorizzi quella subnet:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24Il significato documentato è preciso: subnet separate da virgole a cui Gluetun e i container che condividono il suo network stack possono accedere.
Le connessioni in ingresso da Internet sono un problema distinto. I peer di un client torrent arrivano dal lato VPN, quindi pubblicare la porta 6881 sull'host non produce alcun effetto. Serve una porta inoltrata dal provider e devi indicarla in FIREWALL_VPN_INPUT_PORTS, che consente le porte dal lato del server VPN. È questo il punto che la maggior parte degli stack multimediali creati con Docker Compose lascia non configurato.
Il kill switch: cosa accade quando il tunnel cade
Questo schema giustifica la sua complessità quando si verifica un errore. Il container collegato non dispone di una seconda route. L'unico percorso per uscire dalla macchina passa dal namespace condiviso, quindi, quando il tunnel è inattivo, non esiste alcun percorso alternativo. Il firewall di Gluetun applica la stessa regola dall'altro lato: il traffico in uscita passa attraverso il tunnel o raggiunge l'endpoint del server VPN; tutto il resto viene scartato. Non esiste una finestra in cui i pacchetti possano uscire dall'interfaccia non protetta mentre il client tenta di riconnettersi.
Gluetun monitora la propria connessione. Ogni minuto invia un echo ICMP (un ping) agli indirizzi in HEALTH_ICMP_TARGET_IPS, che per impostazione predefinita è 1.1.1.1,8.8.8.8. Ogni cinque minuti apre una connessione TCP e TLS (Transport Layer Security) completa verso HEALTH_TARGET_ADDRESSES, con valore predefinito cloudflare.com:443,github.com:443. Se questi controlli falliscono, riavvia la VPN all'interno del container e lo registra nei log:
WARN [vpn] restarting VPN because it failed to pass the healthcheck: periodic check: dialing: dial tcp4: lookup cloudflare.com: i/o timeoutLeggete i log del container collegato tenendo presente questo ordine causale. Righe come connection refused, operation not permitted e i/o timeout all'interno dell'applicazione sono conseguenze di un tunnel inattivo, non le cause. La documentazione di Gluetun lo afferma esplicitamente, perché spesso si segnala la conseguenza e la si analizza per ore.
HEALTH_RESTART_VPN=on è il valore predefinito e deve rimanere attivo. Disattivatelo solo per eseguire il debug di un problema specifico, perché, quando è disattivato, un tunnel inattivo rimane inattivo.
Ordinamento: impedire l'avvio dello stack prima che il tunnel sia attivo
L'immagine include un healthcheck Docker:
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheckQuesto comando avvia una seconda copia temporanea di gluetun, che interroga il health server dell'istanza in esecuzione su http://127.0.0.1:9999/. Un tunnel funzionante risponde con 200 OK. Un tunnel non funzionante risponde con 500 Internal server error e una stringa di errore; dopo un singolo errore, il container viene contrassegnato come non integro.
condition: service_healthy attende questa condizione. Il semplice depends_on: [gluetun] attende soltanto l'avvio del container, che avviene diversi secondi prima del completamento dell'handshake. Di conseguenza, l'applicazione viene avviata con la rete non disponibile e spesso rinuncia al primo tentativo di connessione. Healthcheck in Docker Compose illustra la sintassi e i campi relativi alle tempistiche.
C'è un limite che spesso crea problemi. Compose valuta questa condizione una sola volta, quando crea il container. In seguito non arresta né riavvia l'applicazione se gluetun diventa non integro. In questo caso interviene l'auto-healing interno di gluetun, che riavvia il processo VPN invece del container.
Verificare la presenza di una perdita DNS prima di considerare corretta la configurazione
Il DNS (domain name system) è la perdita che persiste anche quando il tunnel è configurato correttamente. Gluetun esegue il proprio resolver all'interno del namespace e inoltra le query tramite DoT (DNS over TLS) a Cloudflare per impostazione predefinita: DNS_UPSTREAM_RESOLVER_TYPE=dot e DNS_UPSTREAM_RESOLVERS=cloudflare. Lasciando invariati entrambi, le query vengono cifrate e transitano attraverso il tunnel.
L'impostazione che causa il problema è DNS_UPSTREAM_PLAIN_ADDRESSES. Si tende a usarla quando un nome non viene risolto e si vuole che risponda il router o il resolver del provider. La documentazione di Gluetun indica chiaramente la conseguenza: tutto il traffico DNS non passerà attraverso il tunnel VPN e ne uscirà al di fuori. Il traffico resta privato. L'elenco dei nomi host no. La stessa configurazione errata con WireGuard è descritta in DNS che smette di risolvere attraverso un tunnel WireGuard.
Per eseguire il test, imposta HTTPPROXY=on su gluetun ed esponi 8888:8888/tcp, quindi punta un browser a quel proxy e apri un test per le perdite DNS. Il risultato dovrebbe indicare il tuo provider o Cloudflare, mai il router di casa. La documentazione di Gluetun avverte che alcuni test per le perdite possono mostrare risultati anomali, perché il resolver all'interno del namespace è un intermediario locale con caching, non il server che fornisce la risposta finale. Considera un paese errato o il resolver del tuo ISP come il segnale effettivo.
Aggiungere Tailscale accanto al sidecar VPN e stabilire quale prevale
Tailscale è una rete overlay basata su WireGuard che consente di raggiungere i propri computer. Viene spesso eseguito accanto a una VPN del provider per mantenere un accesso amministrativo allo stack. I due componenti raramente entrano in conflitto, per un motivo preciso. La documentazione di Tailscale indica il comportamento predefinito: Tailscale opera come rete overlay, instrada il traffico soltanto tra dispositivi che eseguono Tailscale e non modifica il traffico Internet pubblico.
La risposta dipende quindi da una sola impostazione.
- Tailscale nel proprio container, con la configurazione predefinita: non vede mai il traffico in uscita dell'applicazione. Se ne occupa Gluetun. Tailscale raggiunge l'applicazione tramite
gluetun:8080, esattamente come qualsiasi altro container esterno. - Tailscale collegato al namespace di gluetun con
network_mode: "service:gluetun": deve ricevere il propriocap_adddinet_adminenet_raw, perché le capability non vengono ereditate dal namespace. Nella modalità predefinita di rete userspace,TS_USERSPACEè attivo, tailscaled non crea alcuna interfaccia e funziona come proxy SOCKS5 o HTTP; di conseguenza non può modificare il routing. Gluetun continua a gestire tutto il traffico. - Lo stesso caso, con
TS_USERSPACE=false: tailscaled crea un dispositivo tunnel e installa le route, ma soltanto per l'intervallo tailnet100.64.0.0/10e per le eventuali route di subnet pubblicizzate conTS_ROUTES. Il traffico pubblico continua a uscire tramite gluetun. - Uno qualsiasi dei casi precedenti con un exit node selezionato,
sudo tailscale set --exit-node=<exit-node-ip>: Tailscale acquisisce la route predefinita e prevale. Non combinarlo con gluetun. Una sola route predefinita, un solo proprietario.
Se il punto sono le route pubblicizzate e vuoi rendere raggiungibile l'intera rete privata dietro il server, non soltanto il server stesso, eseguire un router di subnet Tailscale su un VPS illustra l'approvazione delle route, l'IP forwarding e il flag lato client che TS_ROUTES, da solo, non configura.
Se Tailscale serve a fornirti un URL amministrativo anziché una route, tailscale serve e tailscale funnel mettono HTTPS davanti a gluetun:8080 per la tua tailnet. Solo funnel lo espone a Internet pubblico.
Un effetto collaterale è visibile quando Tailscale viene eseguito all'interno del tunnel. I peer vedono l'indirizzo del provider VPN, quindi è normale che Tailscale ricorra più spesso ai relay. tailscale status mostra relay "..." accanto a un peer anziché direct quando ciò si verifica. La connessione funziona, ma è più lenta. Se ti serve soltanto la rete overlay, la differenza tra WireGuard semplice e Tailscale è il punto di partenza migliore.
Cosa non funziona e quale messaggio verrà visualizzato
Docker rifiuta di creare il container dell'applicazione. Error response from daemon: conflicting options: port publishing and the container type network mode indica che un blocco ports: è ancora associato al servizio. Spostalo su gluetun.
Compose rifiuta l'intero file. Un servizio non può impostare contemporaneamente network_mode e networks. Inserisci le reti in gluetun.
Un altro container non riesce a risolvere il nome dell'applicazione. curl: (6) Could not resolve host: qbittorrent è il comportamento previsto, perché il container associato non è entrato in alcuna rete e non ha registrato alcun nome. Usa gluetun e la porta.
Il secondo container associato non si avvia. Due processi nello stesso namespace non possono associare la stessa porta. Il processo che perde la contesa segnala che l'indirizzo è già in uso. Modifica la porta interna dell'applicazione oppure esegui una seconda istanza di gluetun.
L'applicazione non ha più accesso alla rete dopo una modifica a gluetun. Il riavvio o la ricreazione di gluetun interrompe la connettività per tutti i container associati. Riavvia quei container.
Le pagine piccole vengono caricate, ma quelle grandi si bloccano. Il problema riguarda l'MTU (maximum transmission unit). Il tunnel aggiunge overhead e un elemento del percorso scarta i pacchetti troppo grandi senza restituire un errore. Riduci WIREGUARD_MTU, prova 1400, quindi 1320.
Gluetun non raggiunge mai lo stato healthy. Il controllo di avvio indica i primi elementi da verificare: WARN [vpn] restarting VPN because it failed to pass the healthcheck: startup check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout. Controlla se la chiave è scaduta, poi se l'elenco dei server è obsoleto e infine se il firewall dell'host blocca il traffico UDP in uscita.
FAQ
Perché le porte pubblicate del mio container hanno smesso di funzionare dietro Gluetun?
Perché network_mode: "service:gluetun" inserisce il container nel namespace di rete di gluetun, e un namespace ha un solo indirizzo IP e un solo insieme di porte in ascolto. L'applicazione continua a essere in ascolto, ma la regola di pubblicazione deve appartenere al container che possiede il namespace. Sposta l'elenco ports: nel servizio gluetun. Se lo hai lasciato sul servizio collegato, Docker non lo creerà nemmeno: Error response from daemon: conflicting options: port publishing and the container type network mode.
Come posso raggiungere da un container esterno alla VPN un container che si trova nel tunnel VPN?
Usa il nome del servizio gluetun e la porta su cui l'applicazione è in ascolto, ad esempio gluetun:8080. Il container collegato non appartiene ad alcuna rete Docker propria, quindi il suo nome non viene mai risolto. Per il traffico tra container non è necessario pubblicare alcuna porta. Nella direzione opposta, su Gluetun v3.41 e versioni successive, un container all'interno del namespace può raggiungere un container esterno tramite il nome del servizio, ad esempio postgres:5432. Un client in una subnet diversa, ad esempio un laptop sulla LAN, viene bloccato dal firewall di gluetun finché non aggiungi quella subnet a FIREWALL_OUTBOUND_SUBNETS.
Gluetun funziona come kill switch quando la VPN si disconnette?
Sì, per due motivi. Il container collegato non ha route oltre a quella disponibile nel namespace condiviso, quindi un tunnel non attivo lo lascia senza un percorso per uscire dalla macchina. Inoltre, il firewall di Gluetun consente il traffico in uscita soltanto attraverso il tunnel e verso l'endpoint del server VPN. Gluetun riavvia quindi internamente la VPN e registra WARN [vpn] restarting VPN because it failed to pass the healthcheck, invece di terminare, perché ogni container collegato perde la rete quando gluetun viene riavviato.
Tailscale e Gluetun nello stesso stack: quale dei due gestisce il traffico in uscita?
Gluetun, in tutte le configurazioni tranne una. Per impostazione predefinita, Tailscale instrada soltanto il traffico tra i dispositivi della tailnet e lascia invariato il traffico pubblico. Nella modalità userspace predefinita dell'immagine del container non crea alcuna interfaccia, quindi non può influire sul routing. Con TS_USERSPACE=false installa soltanto le route per 100.64.0.0/10 e per le subnet pubblicizzate. L'eccezione è un exit node: sudo tailscale set --exit-node=<exit-node-ip> fa diventare Tailscale la route predefinita, che quindi prevale. Scegli un solo prodotto per gestire la route predefinita, invece di sovrapporli.