Redes no Docker Compose: DNS, host e portas
Entenda a rede bridge padrão, o DNS pelo nome do serviço, o modo host, redes entre projetos e a porta publicada que pode ignorar as regras do UFW.
O que o Compose cria antes de iniciar a sua aplicação
A rede do Docker Compose começa com uma regra: docker compose up cria uma rede privada para o projeto, liga todos os serviços a ela e permite que esses serviços comuniquem entre si pelo nome do serviço. Não é necessário escrever uma única linha networks: para obter esse comportamento. Grande parte da confusão sobre as redes do Compose resulta de não saber que esse comportamento já existe por predefinição.
Eis um ficheiro pequeno. Guarde-o como compose.yaml num diretório chamado shop.
services:
web:
image: nginx:1.27
ports:
- "8080:80"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: exampleInicie o projeto e veja o que o Docker criou:
docker compose up -d
docker network lsA lista passa a incluir uma rede chamada shop_default. O Compose atribui-lhe o nome <project>_default, e o nome do projeto corresponde, por predefinição, ao nome do diretório em minúsculas. Substitua esse nome com docker compose -p myproject up -d ou com um name: myproject de nível superior no ficheiro. O driver é bridge, que corresponde a um switch virtual dentro do host. Cada contentor recebe um endereço numa sub-rede privada, e o tráfego de saída é traduzido para o endereço do host no percurso para o exterior.
docker compose down elimina novamente essa rede. É por isso que um contentor antigo de um projeto anterior pode manter uma rede aberta: o Docker recusa a operação com error while removing network: network shop_default has active endpoints, e a solução é parar ou remover o contentor que ainda está ligado a ela.
Se o Compose é novidade para si, vale a pena ler primeiro a estrutura do ficheiro Compose e os comandos do ciclo de vida, porque tudo o que se segue pressupõe que consegue iniciar e parar um projeto.
O DNS por nome de serviço é a parte que os iniciantes não percebem
Em qualquer rede definida pelo utilizador, o Docker executa um servidor DNS incorporado que cada contentor vê em 127.0.0.11. Esse servidor resolve os nomes dos serviços para os endereços atuais dos contentores. Assim, web liga-se à base de dados através do nome de anfitrião db, na porta 5432, sem qualquer configuração.
docker compose exec web getent hosts dbIsto apresenta uma linha semelhante a 172.18.0.2 db. Se não apresentar nada, os dois serviços não estão na mesma rede.
O erro que quase toda a gente comete uma vez é usar localhost na configuração da aplicação. Dentro de um contentor, localhost identifica esse contentor, não o host nem o outro serviço. Os clientes PostgreSQL indicam isso claramente:
could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP connections on port 5432?A cadeia de ligação deve ser postgresql://postgres:example@db:5432/postgres. A parte correspondente ao host é o nome do serviço.
Há dois detalhes que poupam tempo mais tarde. Os nomes são resolvidos para o que está a executar no momento. Por isso, docker compose up -d --scale web=3 fornece um nome com três endereços, e um cliente que mantenha o DNS em cache indefinidamente ficará associado a um contentor que já não existe. Além disso, a rede bridge legada, usada por um docker run simples sem --network, não tem resolução de nomes. Por isso, as recomendações sobre links entre contentores de 2016 não correspondem ao que observa.
Não é necessário usar ports: para ligar dois serviços
ports: publica uma porta do contentor no host. Serve para tráfego que chega de fora do Docker. Não tem qualquer relação com o tráfego entre serviços, que já funciona em todo o intervalo de portas da rede do projeto.
Por isso, o ports: - "5432:5432" que muitas pessoas adicionam ao serviço da base de dados não traz qualquer benefício e causa um problema real: expõe o Postgres na interface pública do servidor. Remova-o. Se precisar de aceder ao serviço a partir do portátil para fazer uma migração, associe-o à interface de loopback com "127.0.0.1:5432:5432" e aceda através de um túnel SSH. A diferença entre um socket em escuta, uma porta publicada e uma regra de firewall é explicada em como funcionam as portas e os serviços em escuta no Linux.
expose: serve apenas como documentação no Compose. Não abre nada, porque não havia nada fechado entre contentores na mesma rede.
Quando o network_mode host é adequado e quais são os custos
O modo host elimina o próprio namespace de rede do contentor e permite que o processo utilize diretamente as interfaces do host.
services:
probe:
image: alpine:3.20
network_mode: host
command: sleep infinityExistem motivos concretos para utilizar este modo. Um processo que precisa de ver tráfego broadcast ou multicast na rede local, como a descoberta de dispositivos de um servidor multimédia ou de um hub de automação doméstica, não consegue vê-lo atrás de uma bridge, porque a bridge não encaminha esse tráfego para o contentor. Um agente de monitorização que lê os contadores das interfaces do host precisa das interfaces do host. Além disso, elimina o salto de tradução de endereços, o que é relevante com taxas elevadas de pacotes.
Os custos são específicos.
ports: deixa de funcionar. O Docker avisa que as portas publicadas são descartadas ao utilizar o modo de rede host, e o contentor faz bind àquilo a que o processo fizer bind. Dois contentores em modo host que tentem utilizar a porta 8080 entram em conflito, e o segundo termina com bind: address already in use.
A resolução de nomes por nome de serviço deixa de funcionar nos dois sentidos. O contentor não está na rede do projeto, por isso não consegue resolver db, e os outros serviços também não o conseguem resolver. Só consegue aceder-lhes através das portas publicadas no host, normalmente em 127.0.0.1.
O isolamento deixa de existir. Um processo que faça bind a 0.0.0.0 dentro de um contentor em modo host fica à escuta em todas as interfaces do servidor, incluindo a interface pública, exatamente como um pacote instalado com apt. Existe uma vantagem: este tráfego segue o percurso normal de entrada, por isso as regras do UFW aplicam-se a ele, o que não acontece com portas publicadas.
O modo host é uma funcionalidade do Docker Engine em Linux. O Docker Desktop só o suporta a partir da versão 4.34 e apenas depois de o ativar. Existem ainda limitações: os contentores não podem fazer bind a endereços IP do host e só são tratados TCP e UDP. Se metade da equipa utiliza servidores Linux e a outra metade utiliza Docker Desktop, espere que o mesmo ficheiro tenha comportamentos diferentes.
Utilize o modo host quando precisar das interfaces do host. Não o utilize para corrigir um problema de ligação, porque normalmente substitui um problema por outro mais difícil.
Ligar dois projetos Compose com uma rede externa
Uma rede criada por um projeto não fica visível para outro. Por isso, um reverse proxy em proxy/compose.yaml não consegue ver uma aplicação em app/compose.yaml, mesmo no mesmo servidor. A solução é usar uma rede que não pertença a nenhum dos projetos.
Crie-a uma vez, manualmente:
docker network create edgeDepois, declare-a como externa em cada projeto. No lado do proxy:
services:
proxy:
image: traefik:v3.1
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- edge
networks:
edge:
name: edge
external: trueNo lado da aplicação:
services:
app:
image: nginx:1.27
networks:
- edge
- internal
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
networks:
- internal
networks:
edge:
name: edge
external: true
internal:external: true informa ao Compose para se ligar a uma rede existente em vez de criar uma, e para a deixar no local em docker compose down. A chave name: separada é mais importante do que parece: sem ela, o Compose procura uma rede chamada exatamente edge. Com ela, pode atribuir um nome à rede no ficheiro e outro nome no host.
Se a rede não existir, o Compose recusa-se a iniciar e informa que a rede foi declarada como externa, mas não foi encontrada. Crie-a primeiro.
Observe o que o ficheiro da aplicação faz com internal. A base de dados fica apenas nessa rede local do projeto, por isso o proxy não consegue aceder-lhe e apenas app consegue. Adicionar internal: true numa rede vai mais longe e remove completamente a rota dessa rede para o exterior. Esta é uma boa predefinição para uma base de dados, mas há um custo que deve conhecer antes de a definir: um contentor numa rede interna não consegue descarregar nada. Por isso, um entrypoint que execute apt-get update ou pip install no arranque ficará bloqueado e depois falhará com um timeout.
Para ver uma configuração completa, com regras de encaminhamento e certificados, consulte executar várias aplicações atrás de uma instância do Traefik.
Portas publicadas contornam o UFW
Esta é a parte da rede do Compose que pode transformar-se num incidente de segurança. Publica uma porta, confirma que o UFW está ativo e bloqueia tudo exceto o SSH, mas o serviço continua acessível a partir da Internet.
sudo ufw status
curl http://203.0.113.10:8080O UFW indica que a porta está bloqueada. O curl devolve a página mesmo assim. Nada está avariado. O Docker escreve as suas próprias regras de tradução de endereços e encaminhamento diretamente no iptables. O tráfego destinado a uma porta publicada do contentor é encaminhado para o contentor, em vez de ser entregue ao host. Por isso, nunca passa pela cadeia que o UFW gere para o tráfego destinado localmente. As regras do Docker também são avaliadas antes das regras do UFW.
A correção curta é publicar apenas onde for necessário:
ports:
- "127.0.0.1:8080:80"Isto associa o lado do host à interface de loopback. A porta fica acessível a partir do próprio servidor e através de um túnel SSH, mas não a partir de outros locais. Coloque o ponto de entrada público atrás de um reverse proxy que publique deliberadamente as portas 80 e 443. A explicação completa, incluindo a cadeia DOCKER-USER para os casos em que é necessário filtrar uma porta publicada, está em por que o Docker publica diretamente além do UFW e como corrigir isso.
Como depurar o problema com quatro comandos
Comece por verificar em que rede cada contentor está realmente ligado:
docker network inspect shop_defaultO bloco Containers lista todos os contentores ligados, incluindo o respetivo endereço. Um serviço ausente dessa lista está noutra rede, está em modo host ou não está em execução.
Teste a resolução de nomes a partir de um contentor temporário ligado à mesma rede. Assim, não precisa de instalar ferramentas nas suas próprias imagens:
docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432Se nslookup falhar, o problema está na resolução de nomes ou na associação à rede. Se nslookup funcionar, mas nc falhar, o serviço está em execução, mas não está a escutar nessa porta ou está a escutar em 127.0.0.1 dentro do próprio contentor em vez de 0.0.0.0. Isto é comum em servidores de desenvolvimento. A correção deve ser feita no endereço de ligação da aplicação, não no Docker.
Há outra falha que pode parecer um erro do Docker. Se os contentores comunicarem entre si, mas não conseguirem aceder a uma máquina da rede do escritório ou da VPN, é provável que a sub-rede do Docker esteja sobreposta a essa rede. Por predefinição, o Docker aloca sub-redes a partir de 172.17.0.0/16. Altere o conjunto em /etc/docker/daemon.json:
{
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 }
]
}Depois, execute sudo systemctl restart docker e recrie as redes afetadas, porque uma rede existente mantém a sub-rede atribuída quando foi criada.
FAQ
Por que os meus containers não conseguem comunicar entre si pelo nome do serviço?
Eles não estão na mesma rede. O Compose coloca todos os serviços automaticamente em <project>_default, mas, assim que adiciona uma lista networks: a um serviço, essa lista passa a ser o conjunto completo de redes desse serviço e a rede predefinida deixa de estar implícita. Execute docker network inspect <network> e confirme que ambos os containers aparecem no bloco Containers. Verifique também se nenhum dos serviços usa network_mode: host, porque um container em modo host não está ligado a nenhuma rede Docker e não consegue resolver nomes de serviços.
Preciso publicar portas para um serviço comunicar com outro?
Não. Numa rede Compose, todas as portas de todos os containers estão acessíveis aos outros containers dessa rede. ports: existe apenas para expor um container ao tráfego proveniente do exterior do Docker, e expose: serve apenas como documentação. Publicar a porta de uma base de dados é um hábito comum e dispendioso, porque coloca a base de dados na interface pública do servidor.
Qual é a diferença entre a rede bridge e a rede host?
A bridge dá ao container o seu próprio namespace de rede e um endereço num switch virtual, com resolução automática de nomes entre containers e tradução do tráfego de saída. A host dá ao container acesso direto à pilha de rede do host: não há um endereço separado, resolução por nome de serviço, publicação de portas nem isolamento em relação aos outros listeners do host. A bridge é a opção predefinida e a correta, exceto quando o processo precisa das interfaces do host.
Como ligo containers de dois ficheiros Compose diferentes?
Crie uma rede partilhada com docker network create edge. Depois, declare-a nos dois ficheiros com external: true e ligue os serviços que precisam de comunicar. O Compose não a cria nem a elimina. Se ignorar a etapa de criação, o Compose recusa iniciar e indica que a rede foi declarada como externa, mas não foi encontrada.
Por que o meu container está acessível pela Internet quando o UFW bloqueia a porta?
Porque uma porta publicada é processada pelas regras de encaminhamento que o Docker adiciona ao iptables. Essas regras são avaliadas antes das regras do UFW, e o tráfego encaminhado também não passa pela chain que o UFW filtra. Ligue o lado do host ao loopback com "127.0.0.1:8080:80" e coloque tudo o que for público atrás de um reverse proxy nas portas 80 e 443.