Gluetun: raggiungere host e altri container
Un container dietro Gluetun non ha interfacce proprie: pubblica le porte su Gluetun e abilita solo le subnet necessarie fuori dal tunnel VPN.
Cosa accade quando un container entra nella rete di gluetun
Un container che imposta network_mode: service:gluetun non dispone di interfacce di rete proprie. Entra nel namespace di rete di gluetun, quindi la pubblicazione delle porte e le regole del firewall non sono più proprietà di quel container, ma del servizio gluetun. Tutte le risposte seguenti derivano da questo unico fatto.
Un namespace di rete è una copia privata dello stack di rete del kernel: ha le proprie interfacce, la propria tabella di routing, le proprie regole del firewall e i propri socket in ascolto. Docker assegna normalmente un namespace a ogni container. Quando si usa network_mode: service:gluetun, Docker salta questo passaggio e inserisce il nuovo container nel namespace già assegnato a gluetun. Il container mantiene il proprio filesystem e il proprio file /etc/hosts, e questo secondo elemento sarà importante più avanti.
È possibile verificarlo direttamente.
docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrentIl comando stampa container: seguito dall'ID del container gluetun, mentre con un container normale stamperebbe bridge. Questa guida riprende il punto in cui si interrompe instradare il traffico Docker attraverso una VPN con gluetun: il tunnel funziona, ma ora nessuno riesce a comunicare con il container.
Pubblicare la porta su gluetun, non sull'applicazione
Lasciare un blocco ports: sul servizio che imposta network_mode impedisce a Docker di creare il container:
Error response from daemon: conflicting options: port publishing and the container type network modeIl motivo è diretto. Pubblicare una porta significa aggiungere una regola NAT (network address translation) che inoltra una porta dell'host allo spazio dei nomi di rete del container, ma questo container non dispone di uno spazio dei nomi proprio. Sposta il mapping sul servizio gluetun. Il numero della porta non cambia, perché l'applicazione continua ad ascoltare su quella porta nello spazio dei nomi condiviso.
services:
gluetun:
ports:
- "8080:8080/tcp" # qBittorrent web UI
qbittorrent:
network_mode: "service:gluetun"
# no ports: block hereUn blocco expose: sul servizio dipendente è altrettanto inutile, mentre un blocco networks: sullo stesso servizio blocca completamente l'avvio: Compose segnala che il servizio dichiara network_mode e networks, che sono mutuamente esclusivi, e rifiuta di caricare il file.
Una conseguenza si manifesta in seguito. Tutti i container nello spazio dei nomi condividono un unico spazio delle porte. Pertanto, due applicazioni che usano entrambe 8080 come porta predefinita entrano in conflitto e quella avviata per seconda non parte, con un errore che indica che l'indirizzo è già in uso. Modifica una delle due porte nella configurazione dell'applicazione, ad esempio la variabile WEBUI_PORT nell'immagine LinuxServer qBittorrent, quindi pubblica il nuovo numero su gluetun.
Come comunicano tra loro i container dietro gluetun?
All'interno del namespace condividono già un'interfaccia di loopback. Un container dietro gluetun raggiunge il container associato all'indirizzo 127.0.0.1:<port> senza usare alcuna rete Docker.
Dall'esterno del namespace, il container non ha un nome. Il DNS integrato di Docker risolve il nome di un servizio nell'indirizzo di quel servizio su una rete definita dall'utente, ma questo container non ha un indirizzo su alcuna rete. Di conseguenza, un container normale come Sonarr non raggiunge il client torrent all'indirizzo http://qbittorrent:8080. Lo raggiunge all'indirizzo http://gluetun:8080, perché il socket è in ascolto nel namespace di gluetun, sull'indirizzo di gluetun. Questo comportamento sorprende chi sa come funzionano le reti e i nomi dei servizi in Docker Compose e si aspetta che si applichi la consueta risoluzione dei nomi. Funziona anche senza pubblicare porte sull'host, perché entrambi i container si trovano sulla stessa rete Compose.
Controllate il DNS prima di eseguire qualsiasi altra attività di troubleshooting. Gluetun usa un resolver proprio e riscrive /etc/resolv.conf nel proprio container, ma /etc/resolv.conf è un file specifico del singolo container. Di conseguenza, il file scritto da gluetun non è quello letto dall'applicazione.
docker exec qbittorrent cat /etc/resolv.confCome raggiungo un servizio in esecuzione sull’host Docker?
Usa host.docker.internal. Richiede due impostazioni in due punti diversi, perché ci sono due problemi distinti.
Prima viene il nome. /etc/hosts è specifico per ogni container, quindi la voce extra_hosts deve essere presente nel container dell’applicazione, non in gluetun.
prowlarr:
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"host-gateway è un valore speciale che Docker sostituisce con un indirizzo interno dell’host stesso. In una normale installazione Docker su Linux, questo è l’indirizzo del bridge docker0, comunemente 172.17.0.1. Verifica il tuo indirizzo con ip -4 addr show docker0 sul VPS. Docker Desktop risolve autonomamente questo nome. Per questo le guide scritte per un computer portatile omettono la riga extra_hosts, mentre lo stesso file poi non funziona su un server.
Poi viene la route. Aggiungere il nome indica soltanto al container quale indirizzo usare. Il pacchetto continua a uscire tramite la route predefinita di gluetun, cioè il tunnel, e il firewall di gluetun lo blocca. Il sintomo è una connessione che resta in attesa e poi va in timeout, non una connessione rifiutata. Un rifiuto indica che il pacchetto è arrivato e qualcosa ha risposto negativamente. Un timeout indica che il pacchetto non è mai arrivato.
gluetun:
environment:
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32Controlla quindi che il servizio sull’host sia effettivamente in ascolto su quell’indirizzo. Un server PostgreSQL associato soltanto a 127.0.0.1 non è raggiungibile da alcun container, con o senza tunnel, perché 127.0.0.1 all’interno del namespace è il loopback del namespace stesso. Associalo invece a 172.17.0.1: accetterà connessioni dai container senza esporsi sull’interfaccia pubblica. Verifica con ss -lntp | grep 5432 sull’host.
Cosa cambia effettivamente FIREWALL_OUTBOUND_SUBNETS
La documentazione di gluetun descrive questo parametro come l’elenco, separato da virgole, delle subnet che gluetun e i container che condividono il suo stack di rete possono raggiungere. Specifica inoltre che il parametro modifica il firewall e le route. Entrambi gli aspetti sono importanti. Gluetun aggiunge una route per ogni subnet elencata tramite il gateway del bridge Docker, quindi i pacchetti destinati a quegli indirizzi escono da eth0 invece di passare nel tunnel. Apre anche il firewall per queste subnet, perché altrimenti gluetun elimina il traffico in uscita che non è diretto al server VPN.
Inserisci il valore senza spazi dopo le virgole.
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32Due proprietà sono facili da trascurare. Questa è un’impostazione a livello di namespace, quindi si applica a tutti i container che usano gluetun, non solo a quello a cui stavi pensando. Inoltre riguarda solo il traffico in uscita: controlla le connessioni avviate da un container. Le connessioni in arrivo su una porta pubblicata seguono un percorso diverso e non richiedono alcuna voce in questo parametro.
Accesso all'interfaccia web da un peer Tailscale
Tailscale assegna a ogni macchina un indirizzo nell'intervallo 100.64.0.0/10, riservato al carrier-grade NAT. Le due direzioni richiedono configurazioni diverse.
L'accesso in ingresso è semplice. La pubblicazione di 8080:8080 su gluetun associa quella porta a tutti gli indirizzi dell'host, incluso l'indirizzo dell'interfaccia tailscale0. Un peer può quindi aprire http://<machine-name>:8080 e raggiungere il container. Gluetun non interviene in questo percorso, perché la regola NAT di Docker risiede sull'host, al di fuori del namespace.
Per rendere l'interfaccia accessibile solo tramite la tailnet, associa la porta pubblicata all'indirizzo Tailscale dell'host invece che a tutti gli indirizzi.
ports:
- "100.101.102.103:8080:8080/tcp"Individua questo indirizzo con tailscale ip -4 sull'host. In questo caso l'associazione è un controllo più efficace di una regola firewall, perché la porta non viene mai aperta sull'interfaccia pubblica. Inoltre evita il problema descritto in Pubblicare le porte di Docker aggirando direttamente ufw.
Il traffico in uscita è il punto in cui torna in gioco FIREWALL_OUTBOUND_SUBNETS. Se un container deve connettersi a un peer, aggiungi l'indirizzo del peer e preferisci un /32 per ogni peer invece dell'intero /10. I nomi MagicDNS non vengono risolti all'interno del container, perché il container non usa il resolver dell'host. Usa quindi l'indirizzo numerico 100.x oppure fissalo con una riga extra_hosts. Lo stesso vale quando esegui un tuo server di controllo Tailscale con Headscale.
Un file Compose completo per la struttura più comune
Un client per i download dietro la VPN, 2 interfacce Web accessibili solo sulla tailnet e un container che legge un database PostgreSQL in esecuzione sull'host.
services:
gluetun:
image: qmcgaw/gluetun:latest
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
ports:
- "127.0.0.1:8000:8000/tcp" # gluetun control server, host only
- "100.101.102.103:8080:8080/tcp" # qBittorrent UI, tailnet only
- "100.101.102.103:9696:9696/tcp" # Prowlarr UI, tailnet only
volumes:
- ./gluetun:/gluetun
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
- WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
- SERVER_CITIES=Amsterdam
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,100.64.7.9/32
- TZ=Europe/Amsterdam
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
network_mode: "service:gluetun"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- WEBUI_PORT=8080
volumes:
- ./qbittorrent:/config
- /srv/downloads:/downloads
depends_on:
gluetun:
condition: service_healthy
restart: unless-stopped
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- PROWLARR__POSTGRES__HOST=host.docker.internal
- PROWLARR__POSTGRES__PORT=5432
- PROWLARR__POSTGRES__USER=prowlarr
- PROWLARR__POSTGRES__PASSWORD=${PROWLARR_DB_PASSWORD}
- PROWLARR__POSTGRES__MAINDB=prowlarr-main
- PROWLARR__POSTGRES__LOGDB=prowlarr-log
volumes:
- ./prowlarr:/config
depends_on:
gluetun:
condition: service_healthy
restart: unless-stoppedLeggere il file per comprenderne la struttura, non per i nomi dei prodotti. Entrambe le interfacce Web sono pubblicate su gluetun e associate all'indirizzo tailnet dell'host, quindi rispondono su Tailscale e in nessun altro punto. Solo Prowlarr contiene la riga extra_hosts, perché è il container che risolve host.docker.internal. FIREWALL_OUTBOUND_SUBNETS definisce 2 indirizzi singoli: l'indirizzo del bridge Docker dell'host, che consente a Prowlarr di aprire una connessione al database, e un peer della tailnet.
Il server PostgreSQL è volutamente assente dal file. È in esecuzione sul VPS come normale servizio di sistema e resta in ascolto su 172.17.0.1:5432. È lo stesso livello architetturale di uno stack arr su Docker Compose, con il database spostato fuori da Docker.
Tenere la chiave privata WireGuard fuori dal file Compose. ${WIREGUARD_PRIVATE_KEY} legge i valori da un file .env nella stessa directory, secondo il modello descritto in file env e secret per Docker Compose. La clausola condition: service_healthy usa l'healthcheck già incluso nell'immagine gluetun, quindi nessun servizio viene avviato finché il tunnel non segnala di essere attivo. Gli healthcheck di Compose spiegano la struttura generale.
Pubblicare su ogni indirizzo invece che solo sulla tailnet
Rimuovere il prefisso dell'indirizzo e i binding delle porte su 0.0.0.0, che include l'IP pubblico del VPS. Eseguire questa modifica solo dietro un firewall sotto il proprio controllo e leggere prima la nota su ufw riportata sopra.
ports:
- "8080:8080/tcp"Verificare che il tunnel stia ancora trasportando traffico
Eseguire la stessa richiesta due volte: una dall'interno del namespace e una dall'host, quindi confrontare i risultati.
docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org
curl -s https://api.ipify.orgIl primo comando dovrebbe visualizzare l'indirizzo di uscita del provider VPN. Il secondo dovrebbe visualizzare l'indirizzo del VPS. Se gli indirizzi coincidono, il traffico del container non passa attraverso il tunnel. Fino a quando il problema non viene corretto, tutte le altre correzioni descritte in questa guida sono irrilevanti.
La tabella di routing mostra quale traffico passa dal tunnel e quale lo evita.
docker run --rm --network=container:gluetun alpine:3.22 ip route showLa route predefinita dovrebbe puntare all'interfaccia del tunnel, tun0. Sotto di essa dovrebbe essere presente una route per ogni voce in FIREWALL_OUTBOUND_SUBNETS, con destinazione il gateway del bridge Docker. Qualsiasi altra route che esce tramite eth0 identifica traffico che bypassa la VPN.
Il control server di Gluetun restituisce lo stesso IP pubblico sulla porta 8000, all'indirizzo /v1/publicip/ip. Le versioni recenti richiedono di configurare l'autenticazione per le route del control server; configurarla prima di farvi affidamento.
La fuga causata da una subnet errata
FIREWALL_OUTBOUND_SUBNETS è un'apertura intenzionale nel firewall, quindi la dimensione dell'apertura corrisponde alla dimensione del rischio. Ecco quattro modi per renderla eccessiva:
0.0.0.0/0invia tutto il traffico fuori dal tunnel. I due controlli degli indirizzi IP precedenti rilevano il problema alla prima esecuzione, perché restituiscono lo stesso indirizzo.- Un intervallo più ampio del necessario. Aprire
10.0.0.0/8per raggiungere una macchina all'indirizzo10.0.1.7espone anche tutti gli indirizzi che un peer torrent potrebbe pubblicizzare in quell'intervallo. Specifica10.0.1.7/32. - Un intervallo che si sovrappone agli indirizzi usati dal tunnel. La documentazione di gluetun avverte che, in questo caso, gluetun invia il traffico VPN attraverso il bridge invece di usare il tunnel, interrompendo il port forwarding. Controlla il valore
WIREGUARD_ADDRESSESprima di aprire una rete privata qualsiasi. 100.64.0.0/10per Tailscale. In questo modo vengono aperti circa quattro milioni di indirizzi per raggiungere un solo peer. Elenca i peer necessari come voci/32.
Ricorda che l'impostazione si applica all'intero namespace. Aprire una subnet affinché un indexer possa raggiungere un servizio sull'host apre la stessa subnet anche per il client torrent che condivide quel namespace. Esegui nuovamente il controllo dell'IP pubblico dopo ogni modifica a questa variabile, perché è l'unico test che mostra se la modifica ha prodotto il risultato previsto.
Cosa si interrompe quando riavvii gluetun
gluetun gestisce lo spazio dei nomi, quindi il ciclo di vita di gluetun coincide con quello dello spazio dei nomi. L'avvio di un container dipendente mentre gluetun è inattivo fallisce immediatamente:
Error response from daemon: cannot join network of a non running containerIl riavvio di gluetun senza ricreare il gruppo produce un errore meno evidente. I container dipendenti continuano a essere eseguiti mentre lo spazio dei nomi a cui erano collegati viene ricreato sotto di loro, quindi docker ps segnala che tutto è operativo, ma nessun servizio risponde. Dopo qualsiasi modifica al servizio gluetun, ricrea l'intero gruppo invece di riavviarne una sola parte.
docker compose up -d --force-recreateLo stesso vale per gli aggiornamenti dell'immagine. Se scarichi una nuova immagine gluetun e ricrei solo quel servizio, gli altri container continuano a puntare a uno spazio dei nomi che non esiste più.
FAQ
Perché Docker mostra "port publishing and the container type network mode"?
Perché su un servizio è ancora presente un blocco ports: che imposta anche network_mode: service:gluetun. La pubblicazione di una porta aggiunge una regola NAT che inoltra una porta dell'host allo spazio dei nomi di rete del container. Un container in questa modalità non dispone di uno spazio dei nomi proprio. Elimina il blocco ports: da quel servizio e aggiungi la stessa mappatura al servizio gluetun. Il numero di porta resta invariato, perché l'applicazione continua ad ascoltare su quella porta nello spazio dei nomi condiviso.
Come possono raggiungere un servizio dietro gluetun gli altri container?
I container nello stesso spazio dei nomi si raggiungono tramite 127.0.0.1. I container esterni usano il nome del servizio gluetun, quindi http://gluetun:8080 funziona mentre http://qbittorrent:8080 non funziona. Il container dell'applicazione non ha un indirizzo su alcuna rete Docker. Per questo il server DNS integrato non ha alcun indirizzo da risolvere per il suo nome. Non è necessaria la pubblicazione di una porta, purché entrambi i container condividano una rete Compose.
Cosa devo inserire in FIREWALL_OUTBOUND_SUBNETS?
Inserisci soltanto gli indirizzi a cui un container dietro gluetun deve avviare una connessione, specificandoli nel modo più restrittivo possibile. Un singolo computer si esprime come /32. Le due voci più comuni sono l'host Docker, indicato da 172.17.0.1/32, e un /32 per ogni peer Tailscale che devi raggiungere. Non aggiungere mai 0.0.0.0/0 e non aggiungere intervalli che si sovrappongono agli indirizzi del tunnel VPN. Le connessioni in ingresso a una porta pubblicata non richiedono alcuna voce.
Perché il container non riesce a risolvere i nomi MagicDNS di Tailscale?
MagicDNS funziona configurando il resolver dell'host in modo che usi il server DNS di Tailscale. Il container non usa però il resolver dell'host. Usa il resolver definito nel proprio /etc/resolv.conf, che dietro gluetun corrisponde alla configurazione DNS di gluetun. Verifica con docker exec <container> cat /etc/resolv.conf. Usa l'indirizzo numerico 100.x del peer oppure associa il nome a un indirizzo con una voce extra_hosts nel container interessato.
Come posso verificare che il traffico passi ancora attraverso la VPN?
Esegui una richiesta dallo spazio dei nomi e la stessa richiesta dall'host, quindi confronta le risposte. docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org dovrebbe restituire l'indirizzo di uscita del provider VPN, mentre curl -s https://api.ipify.org sul VPS restituisce l'indirizzo del VPS. Se le due risposte coincidono, il tunnel non trasporta il traffico del container. Ripeti il controllo dopo ogni modifica a FIREWALL_OUTBOUND_SUBNETS.