Docker via VPN: portas somem com o Gluetun
Ao usar network_mode: service:gluetun, as portas publicadas e o nome de serviço desaparecem. Veja o erro exato e um compose funcional com Gluetun v3.41.3.
Porque desaparecem as portas quando encaminha contentores Docker através de uma VPN
Para encaminhar contentores Docker através de uma VPN, atribua o túnel a um contentor e ligue os restantes ao respetivo namespace de rede com network_mode: "service:gluetun". É esta ligação que costuma causar dúvidas. O contentor ligado deixa de ter uma rede própria, pelo que perde as portas publicadas e o nome de serviço Docker. Publique as portas no contentor da VPN. Os outros contentores chegam à aplicação através do nome do contentor da VPN.
Se deixar um bloco ports: no contentor ligado, o Docker recusa-se a criá-lo:
Error response from daemon: conflicting options: port publishing and the container type network modeA ferramenta usada aqui é o Gluetun, um contentor que estabelece ligação a um fornecedor comercial de VPN (rede privada virtual) através de WireGuard ou OpenVPN e inclui a sua própria firewall. A versão v3.41.3 é a atual em agosto de 2026. Os exemplos usam Mullvad com WireGuard, pelo que precisa de uma conta e de uma chave fornecidas pelo seu fornecedor. Se preferir terminar o túnel em hardware que controla, executar o seu próprio servidor WireGuard numa VPS cria a outra extremidade, e wg-easy no Docker disponibiliza essa configuração através de uma interface web.
O que network_mode: "service:gluetun" faz realmente
Normalmente, cada contentor Docker recebe o seu próprio namespace de rede: as suas próprias interfaces, tabela de encaminhamento, regras de firewall e sockets à escuta. O modo service: ignora esse passo e inicia o contentor dentro do namespace do gluetun. Um namespace significa um endereço IP, e isso altera seis aspectos.
- A aplicação não tem um endereço próprio. Usa o endereço do gluetun.
- A aplicação não está ligada a nenhuma rede Docker. Por isso, o seu nome de serviço nunca é registado nem resolvido. Os outros contentores têm de usar
gluetun. - Os contentores dentro do namespace comunicam entre si através de
localhost. - Dois contentores no mesmo namespace não podem escutar na mesma porta. A documentação do Gluetun é clara: não existe uma solução alternativa.
- As capabilities pertencem a um contentor, não a um namespace. O Gluetun tem
NET_ADMINe/dev/net/tunporque cria a interface do túnel. O contentor ligado não as herda. - O Compose rejeita qualquer ficheiro em que um serviço defina simultaneamente
network_modeenetworks. Ligue o gluetun às suas redes para que a aplicação use essa ligação.
Reiniciar o gluetun desliga tudo o que está ligado a ele. Esse comportamento está documentado e é a razão pela qual o gluetun reinicia o processo VPN dentro do contentor, em vez de terminar quando a ligação falha. Depois de reiniciar ou recriar o gluetun manualmente, reinicie os contentores ligados a ele.
O ficheiro compose que funciona
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-stoppedA tag :v3 é a versão estável mais recente da série v3. A tag :latest aponta para o último commit do branch master, que corresponde à versão de desenvolvimento. Por isso, fixe :v3 numa máquina que não pretende depurar numa terça-feira.
WEBUI_PORT=8080 tem de corresponder à porta publicada, porque o qBittorrent faz bind dentro do namespace do gluetun e a regra de publicação encaminha o tráfego do host para a porta 8080 nesse namespace. Se alterar um número sem alterar o outro, a porta não responde. 127.0.0.1:8080:8080 mantém a interface Web no endereço de loopback do host. Um 8080:8080 sem endereço publica em todas as interfaces e cria a sua própria regra de firewall. É assim que as portas publicadas pelo Docker passam diretamente pelo ufw.
Inicie os serviços e verifique-os nesta ordem:
docker compose up -d
docker compose ps
docker compose logs gluetun | tail -30docker compose ps deve mostrar o gluetun como healthy e o qbittorrent como running. Depois, confirme o endereço de saída a partir do namespace. Esta verificação determina o resultado de todas as restantes:
docker run --rm --network=container:gluetun alpine:3.22 sh -c "apk add wget && wget -qO- https://ipinfo.io"O campo ip desse JSON deve conter o endereço do seu fornecedor de VPN. Se contiver o endereço do próprio servidor, a aplicação não está no túnel e nada do que é descrito abaixo funcionará.
Mantenha as chaves fora do ficheiro compose
gluetun.env contém as credenciais e fica fora do git:
WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32Ambos os valores vêm de um ficheiro de configuração do WireGuard que gera na área da sua conta no fornecedor. Defina o ficheiro com o modo 600. Seja claro sobre o que isto oferece: a chave fica fora do seu repositório, mas docker inspect gluetun continua a imprimir todas as variáveis de ambiente para qualquer pessoa que consiga aceder ao socket do Docker. Ficheiros de ambiente e secrets no Docker Compose aborda opções mais robustas.
Como um container fora do túnel comunica com um container dentro dele
As duas direções funcionam, e cada uma usa um nome diferente. Os dois containers precisam de uma rede Docker partilhada. Essa rede é a rede do gluetun, porque o container associado não tem uma rede própria. Como as redes do Docker Compose são ligadas explica os valores predefinidos.
De fora para dentro, use o nome do gluetun e a porta em que a aplicação escuta. Um container de reverse proxy acede à interface web do qBittorrent em gluetun:8080. Não é necessária nenhuma entrada ports:, porque o tráfego entre containers permanece na rede Docker e nunca passa por uma porta do host.
De dentro para fora, use o nome do serviço do outro container, por exemplo postgres:5432. O Gluetun resolve nomes de outros containers a partir do seu namespace desde a v3.41. Fixe essa versão ou uma mais recente se um nome não for resolvido.
A firewall do Gluetun decide quem pode abrir uma ligação para ele. O tráfego proveniente da própria rede Docker do gluetun é permitido. Um cliente noutra sub-rede, um portátil na sua LAN ou um container numa bridge network separada é descartado até essa sub-rede ser indicada:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24O significado documentado é exato: sub-redes separadas por vírgulas às quais o Gluetun e os containers que partilham a sua network stack podem aceder.
As ligações recebidas da Internet são um problema separado. Os peers de um cliente BitTorrent chegam pelo lado da VPN, portanto publicar a porta 6881 no host não tem efeito. É necessária uma porta encaminhada pelo seu provider e essa porta deve ser indicada em FIREWALL_VPN_INPUT_PORTS, que permite portas do lado do servidor VPN. Esta é a parte que a maioria das media stacks criadas com Docker Compose deixa configurada incorretamente.
O interruptor de segurança: o que acontece quando o túnel cai
Este padrão justifica a sua complexidade quando ocorre uma falha. O contentor associado não tem uma segunda rota. O único caminho para sair da máquina é o namespace que partilha. Por isso, quando o túnel está inativo, não existe uma rota alternativa. A firewall do Gluetun aplica a mesma regra no sentido inverso: o tráfego de saída passa pelo túnel ou pelo endpoint do servidor VPN, e todo o restante tráfego é descartado. Não existe um intervalo em que os pacotes possam sair pela interface normal enquanto um cliente restabelece a ligação.
O Gluetun monitoriza a própria ligação. A cada minuto, envia um eco ICMP (um ping) para os endereços em HEALTH_ICMP_TARGET_IPS, que, por predefinição, são 1.1.1.1,8.8.8.8. A cada cinco minutos, estabelece uma ligação TCP e TLS (segurança da camada de transporte) completa para HEALTH_TARGET_ADDRESSES, com o valor predefinido cloudflare.com:443,github.com:443. Quando essas verificações falham, reinicia a VPN dentro do contentor e regista o evento:
WARN [vpn] restarting VPN because it failed to pass the healthcheck: periodic check: dialing: dial tcp4: lookup cloudflare.com: i/o timeoutLeia os logs do contentor associado tendo essa sequência em mente. Linhas como connection refused, operation not permitted e i/o timeout dentro da aplicação são consequências de um túnel inativo, não as causas. A documentação do Gluetun afirma isto explicitamente, porque é comum os administradores identificarem a consequência e investigarem-na durante horas.
HEALTH_RESTART_VPN=on é o valor predefinido e deve permanecer ativado. Desative-o apenas enquanto investiga uma falha específica, porque, quando está desativado, um túnel inativo permanece inativo.
Ordem: impedir que a stack inicie antes de o túnel estar ativo
A imagem inclui um healthcheck do Docker:
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheckEsse comando executa uma cópia temporária do gluetun, que consulta o servidor de estado da instância em execução em http://127.0.0.1:9999/. Um túnel funcional responde 200 OK. Um túnel com problemas responde 500 Internal server error com uma mensagem de erro, e o contentor é marcado como não saudável após uma única falha.
condition: service_healthy é o mecanismo que aguarda esse resultado. O depends_on: [gluetun] simples aguarda apenas o arranque do contentor. Esse arranque ocorre vários segundos antes de o handshake ser concluído. Por isso, a aplicação inicia com a rede indisponível e muitas vezes desiste na primeira tentativa de ligação. Healthchecks no Docker Compose explica a sintaxe e os campos de temporização.
Há uma limitação que costuma causar problemas. O Compose avalia essa condição uma vez, quando cria o contentor. Não para nem reinicia a aplicação posteriormente se o gluetun ficar não saudável. O mecanismo interno de recuperação automática do gluetun trata esse caso. Por isso, reinicia o processo VPN em vez do contentor.
Verifique se há um vazamento de DNS antes de confiar na configuração
O DNS (domain name system) é o vazamento que persiste mesmo quando o túnel está configurado corretamente. O Gluetun executa o seu próprio resolvedor dentro do namespace e encaminha as consultas por DoT (DNS over TLS) para o Cloudflare por padrão: DNS_UPSTREAM_RESOLVER_TYPE=dot e DNS_UPSTREAM_RESOLVERS=cloudflare. Não altere nenhuma das duas opções. Assim, as consultas são encriptadas e passam pelo túnel.
A configuração que causa este problema é DNS_UPSTREAM_PLAIN_ADDRESSES. Alguns utilizadores recorrem a ela quando um nome não é resolvido e querem que o router ou o resolvedor do fornecedor responda. A documentação do Gluetun explica claramente a consequência: todo o tráfego DNS deixará de passar pelo túnel VPN e sairá por fora dele, expondo-se. O seu tráfego continua privado. A sua lista de nomes de host não. A versão do WireGuard do mesmo erro é abordada em DNS que deixa de ser resolvido através de um túnel WireGuard.
Para testar, defina HTTPPROXY=on no gluetun e publique 8888:8888/tcp. Em seguida, aponte um browser para esse proxy e carregue um teste de vazamento de DNS. O resultado deve identificar o seu fornecedor ou o Cloudflare, nunca o seu router doméstico. A própria documentação do Gluetun avisa que alguns testes de vazamento apresentam resultados inesperados, porque o resolvedor dentro do namespace é um intermediário local de cache, e não o servidor que responde finalmente. Considere como sinal real um país incorreto ou o resolvedor do seu próprio ISP.
Adicionar o Tailscale junto ao sidecar da VPN e determinar qual prevalece
O Tailscale é uma rede de sobreposição baseada em WireGuard para aceder às suas próprias máquinas. É comum executá-lo junto de uma VPN de um fornecedor para manter um caminho de administração para a stack. Os dois raramente entram em conflito, por uma razão que vale a pena compreender. A documentação do Tailscale define o comportamento predefinido: funciona como uma rede de sobreposição, encaminha tráfego apenas entre dispositivos que executam Tailscale e não interfere no tráfego para a Internet pública.
Por isso, a resposta depende de uma configuração.
- Tailscale no seu próprio contentor, com a configuração predefinida: nunca vê o tráfego de saída da aplicação. O Gluetun encaminha todo esse tráfego. O Tailscale acede à aplicação através de
gluetun:8080, exatamente como qualquer outro contentor externo. - Tailscale associado ao namespace do gluetun com
network_mode: "service:gluetun": precisa do seu própriocap_adddenet_adminenet_raw, porque as capabilities não são partilhadas com o namespace. No modo predefinido de rede em userspace,TS_USERSPACEestá ativo, o tailscaled não cria nenhuma interface e funciona como um proxy SOCKS5 ou HTTP, pelo que não pode alterar o encaminhamento. O Gluetun continua a encaminhar todo o tráfego. - O mesmo caso, com
TS_USERSPACE=false: o tailscaled cria um dispositivo de túnel e instala rotas, mas apenas para o intervalo da tailnet100.64.0.0/10e para quaisquer rotas de sub-rede anunciadas comTS_ROUTES. O tráfego público continua a sair através do gluetun. - Qualquer uma das opções anteriores com um exit node selecionado,
sudo tailscale set --exit-node=<exit-node-ip>: o Tailscale assume a rota predefinida e prevalece. Não combine essa configuração com o gluetun. Deve existir uma única rota predefinida e um único responsável por ela.
Existe um efeito secundário quando o Tailscale é executado dentro do túnel. Os peers veem o endereço do fornecedor da VPN, pelo que é normal recorrer a relays com mais frequência. tailscale status mostra relay "..." junto a um peer, em vez de direct, quando isso acontece. A ligação funciona, mas fica mais lenta. Se a única funcionalidade necessária era a rede de sobreposição, a diferença entre WireGuard simples e Tailscale é o melhor ponto de partida.
O que falha e a mensagem apresentada
O Docker recusa-se a criar o contentor da aplicação. Error response from daemon: conflicting options: port publishing and the container type network mode significa que ainda existe um bloco ports: no serviço associado. Mova-o para o gluetun.
O Compose recusa o ficheiro inteiro. Um serviço não pode definir network_mode e networks em simultâneo. Coloque as redes no gluetun.
Outro contentor não consegue resolver a aplicação. curl: (6) Could not resolve host: qbittorrent é o comportamento correto, porque o contentor associado não entrou em nenhuma rede nem registou nenhum nome. Use gluetun e a porta.
O segundo contentor associado não inicia. Dois processos no mesmo namespace não podem associar a mesma porta. O processo que perde indica que o endereço já está a ser utilizado. Altere a porta interna da aplicação ou execute um segundo gluetun.
A aplicação fica sem rede depois de alterar o gluetun. Reiniciar ou recriar o gluetun interrompe a conectividade de tudo o que está associado a ele. Reinicie esses contentores.
As páginas pequenas carregam, mas as grandes ficam bloqueadas. Isso é MTU (unidade máxima de transmissão). O túnel adiciona overhead, e algum ponto do caminho descarta os pacotes demasiado grandes sem devolver um erro. Reduza WIREGUARD_MTU, experimente 1400 e depois 1320.
O Gluetun nunca fica saudável. A verificação de arranque indica as primeiras causas a investigar: WARN [vpn] restarting VPN because it failed to pass the healthcheck: startup check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout. Verifique se a chave expirou. Depois, confirme se a lista de servidores está desatualizada. Por fim, confirme se a firewall do host bloqueia tráfego UDP de saída.
FAQ
Por que as portas publicadas do meu container deixaram de funcionar atrás do Gluetun?
Porque network_mode: "service:gluetun" coloca o container no namespace de rede do gluetun, e um namespace tem um único endereço IP e um único conjunto de portas em escuta. A aplicação continua a escutar, mas a regra de publicação tem de ficar no container que possui o namespace. Mova a lista ports: para o serviço gluetun. Se a deixou no serviço associado, o Docker nem sequer a cria: Error response from daemon: conflicting options: port publishing and the container type network mode.
Como acedo a um container dentro do túnel VPN a partir de um container fora dele?
Use o nome do serviço do gluetun e a porta em que a aplicação escuta, por exemplo gluetun:8080. O container associado não está ligado à sua própria rede Docker, por isso o seu próprio nome nunca é resolvido. Não é necessário publicar portas para o tráfego entre containers. No sentido inverso, um container dentro do namespace acede a um container externo pelo nome do serviço, como postgres:5432, no Gluetun v3.41 e posteriores. Um cliente noutra sub-rede, como um portátil na sua LAN, é bloqueado pela firewall do gluetun até adicionar essa sub-rede a FIREWALL_OUTBOUND_SUBNETS.
O Gluetun funciona como um kill switch quando a VPN cai?
Sim, por duas razões. O container associado não tem outra rota além da existente no namespace partilhado, por isso um túnel interrompido deixa-o sem caminho para sair da máquina. A firewall do Gluetun também permite tráfego de saída apenas através do túnel e para o endpoint do servidor VPN. O Gluetun reinicia internamente a VPN e regista WARN [vpn] restarting VPN because it failed to pass the healthcheck, em vez de terminar, porque todos os containers associados perdem a rede quando o próprio gluetun reinicia.
Tailscale e Gluetun na mesma stack: qual deles encaminha o tráfego de saída?
O Gluetun, em todas as configurações exceto uma. Por predefinição, o Tailscale encaminha apenas o tráfego entre dispositivos da sua tailnet e deixa o tráfego público inalterado. No modo userspace predefinido da imagem do container, não cria qualquer interface, por isso não pode afetar o encaminhamento. Com TS_USERSPACE=false, instala rotas apenas para 100.64.0.0/10 e para as suas sub-redes anunciadas. A exceção é um exit node: sudo tailscale set --exit-node=<exit-node-ip> torna o Tailscale a rota predefinida, que passa a ser utilizada. Escolha um único produto para gerir a rota predefinida, em vez de usar ambos em conjunto.