Um Guia Prático para Redes Docker Personalizadas e Comunicação entre Contêineres

Este guia oferece uma exploração prática das redes bridge personalizadas do Docker e seu papel na comunicação entre contêineres. Aprenda a criar, gerenciar e conectar contêineres usando a CLI do Docker e o Docker Compose. Descubra como redes personalizadas permitem resolução automática de DNS, melhoram o isolamento e simplificam a comunicação entre serviços, resultando em aplicações conteinerizadas mais robustas e escaláveis.

Um Guia Prático para Redes Docker Personalizadas e Comunicação entre Contêineres

As redes Docker personalizadas são um daqueles recursos que parecem opcionais até você executar mais de um contêiner. A bridge padrão pode servir para um teste rápido, mas uma bridge definida pelo usuário oferece nomes de serviço previsíveis, isolamento mais limpo e depuração mais fácil. Para um aplicativo pequeno com um contêiner web, um contêiner de API e um banco de dados, essa diferença é imediata: a API pode se conectar a db:5432 em vez de ficar procurando o IP que o Docker atribuiu hoje.

Este guia foca em redes bridge definidas pelo usuário em um único host Docker. Redes overlay, rede Kubernetes e descoberta de serviços Swarm resolvem problemas relacionados em configurações multi-host, mas a rede bridge ainda é a ferramenta do dia a dia para desenvolvimento local, implantações pequenas e projetos Docker Compose.

Por que a bridge padrão se torna complicada

O Docker cria uma rede chamada bridge automaticamente. Se você executar contêineres sem especificar uma rede, eles geralmente vão parar lá. Funciona para casos simples, mas não é agradável para aplicações com múltiplos contêineres.

Em uma rede bridge definida pelo usuário, o Docker fornece DNS embutido para nomes de contêineres e nomes de serviço do Compose. Na bridge padrão, a descoberta baseada em nomes é limitada e a vinculação legada não é um padrão que você deva adotar. O resultado prático é que redes personalizadas permitem configurar aplicações com nomes de host estáveis:

DATABASE_HOST=db
REDIS_HOST=redis
API_BASE_URL=http://api:3000

Isso é mais fácil de ler, mais fácil de mover entre máquinas e menos frágil do que endereços IP de contêineres.

Redes personalizadas também criam um limite mais claro. Contêineres conectados à mesma rede podem se comunicar entre si. Contêineres em redes diferentes não podem, a menos que você conecte um contêiner a ambas ou publique portas através do host. Isso não é um modelo de segurança completo, mas é uma camada útil de separação.

Criar uma rede com a CLI do Docker

Crie uma bridge definida pelo usuário:

docker network create app-net

Inicie dois contêineres nela:

docker run -d --name db --network app-net -e POSTGRES_PASSWORD=devpass postgres:16
docker run -d --name adminer --network app-net -p 8080:8080 adminer

Do contêiner adminer, o nome do host do banco de dados é db. Você não precisa saber o IP dele.

Inspecione a rede:

docker network inspect app-net

Você verá o driver, a sub-rede, o gateway e os contêineres conectados. Ao depurar, este comando responde a uma pergunta básica: os dois contêineres estão realmente na mesma rede?

Você pode conectar um contêiner existente:

docker network connect app-net some-container

E desconectá-lo:

docker network disconnect app-net some-container

O Docker não removerá uma rede enquanto contêineres ainda estiverem conectados. Desconecte ou remova os contêineres primeiro:

docker network rm app-net

Portas publicadas são diferentes de portas entre contêineres

Uma confusão comum: contêineres na mesma rede Docker não precisam de portas de host publicadas para se comunicar. Portas publicadas são para tráfego que entra do host ou de fora do host.

Se um contêiner de API escuta na porta 3000 e um contêiner web está na mesma rede, o contêiner web pode chamar:

http://api:3000

Você só precisa de -p 3000:3000 se quiser acessar a API do navegador do seu laptop ou de outro host através do host Docker.

Isso significa que seu banco de dados geralmente não deve publicar uma porta de host em uma configuração semelhante à produção, a menos que algo fora do Docker precise de acesso direto. Deixe a API alcançar db:5432 através da rede Docker privada.

Use Compose para aplicativos normais com múltiplos serviços

O Docker Compose cria uma rede padrão para o projeto mesmo que você não defina uma. Os serviços podem se alcançar pelo nome do serviço:

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    depends_on:
      - api

  api:
    image: my-api:latest
    environment:
      DATABASE_HOST: db

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: devpass

Nesse arquivo, api pode alcançar db usando o nome de host db. web pode alcançar api usando o nome de host api, assumindo que a configuração no nível da aplicação aponte para lá.

Você também pode definir redes nomeadas quando quiser uma intenção ou separação mais clara:

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    networks:
      - frontend

  api:
    image: my-api:latest
    environment:
      DATABASE_HOST: db
    networks:
      - frontend
      - backend

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: devpass
    networks:
      - backend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

Aqui, web não pode falar diretamente com db porque eles não compartilham uma rede. api é a ponte entre as duas camadas da aplicação. Esta é uma forma útil para serviços reais: exponha apenas o serviço de borda ao host, mantenha o banco de dados privado e conecte cada serviço apenas onde ele precisa se comunicar.

depends_on não é prontidão

O depends_on do Compose controla a ordem de inicialização no uso comum do Compose, mas não garante que o banco de dados esteja pronto para aceitar conexões. Sua API pode iniciar após o processo do contêiner db começar e ainda falhar porque o PostgreSQL está inicializando.

Lide com a prontidão na aplicação com tentativas repetidas, ou use uma verificação de saúde e uma configuração do Compose que respeite a saúde do serviço para sua versão e fluxo de trabalho do Compose. Mesmo assim, a lógica de repetição no nível da aplicação ainda é o hábito mais confiável porque os bancos de dados podem reiniciar após a inicialização inicial.

Uma configuração prática de API usa DATABASE_HOST=db e tenta a conexão por um curto período antes de sair com um erro claro.

Sub-redes personalizadas são úteis, mas não as use em excesso

Você pode escolher uma sub-rede:

docker network create --subnet 172.28.0.0/16 app-net

No Compose:

networks:
  backend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

Isso ajuda quando a sub-rede automática do Docker se sobrepõe a uma VPN, rede de escritório ou outra rota no host. Não é necessário para a maioria dos projetos. Codificar IPs de contêineres deve ser raro; nomes de serviço geralmente são o contrato melhor.

Solucionar problemas de comunicação de rede

Quando um contêiner não consegue alcançar outro, verifique nesta ordem:

  1. Ambos os contêineres estão em execução?
  2. Eles estão conectados à mesma rede?
  3. O cliente está usando o nome do serviço/contêiner, não localhost?
  4. O servidor está escutando na porta e interface esperadas?
  5. A porta está publicada apenas quando o acesso ao host é necessário?

O erro localhost é especialmente comum. Dentro de um contêiner, localhost significa o mesmo contêiner, não o host Docker e nem outro serviço. Se a API tenta se conectar a localhost:5432, ela está procurando o PostgreSQL dentro do contêiner da API. Use db:5432 quando o serviço de banco de dados se chama db.

Inspecione redes:

docker network inspect app-net

Execute um contêiner de diagnóstico temporário na mesma rede:

docker run --rm -it --network app-net alpine sh

Dentro dele, instale ou use ferramentas disponíveis conforme necessário:

getent hosts db
nc -vz db 5432

Imagens mínimas podem não ter nc, curl ou ferramentas DNS instaladas. Um contêiner de depuração de curta duração geralmente é mais limpo do que adicionar pacotes de solução de problemas à sua imagem de aplicação.

Um padrão padrão sensato

Para a maioria dos aplicativos de host único, use Compose e deixe-o criar a rede do projeto. Adicione redes explícitas quando precisar de separação, como frontend e backend. Use nomes de serviço para tráfego interno. Publique apenas as portas que humanos, proxies reversos ou sistemas externos precisam alcançar.

Isso lhe dá uma configuração que é fácil de explicar:

  • O navegador alcança localhost:8080 porque web publica uma porta.
  • web alcança api através da rede Docker.
  • api alcança db através da rede backend.
  • db não tem porta de host a menos que haja uma razão operacional real.

Redes Docker personalizadas não são apenas um recurso interessante. Elas são a diferença entre contêineres que apenas rodam na mesma máquina e serviços que têm um modelo de comunicação claro.

Aliases de rede podem facilitar migrações

Às vezes, uma aplicação espera um nome de host que você não quer usar como nome de serviço do Compose. Você pode adicionar um alias em uma rede:

services:
  postgres:
    image: postgres:16
    networks:
      backend:
        aliases:
          - database

networks:
  backend:
    driver: bridge

Contêineres em backend agora podem alcançar o serviço como postgres ou database. Isso é útil ao migrar um aplicativo mais antigo que já usa DATABASE_HOST=database, mas eu não usaria aliases em todos os lugares. Nomes de serviço são mais simples quando você controla a configuração da aplicação.

Acesso ao host é um problema separado

Um contêiner falando com outro contêiner é diferente de um contêiner falando com o host Docker. No Docker Desktop, host.docker.internal está comumente disponível. No Linux, o suporte depende da versão e configuração do Docker; muitas equipes o adicionam explicitamente quando necessário:

docker run --add-host=host.docker.internal:host-gateway ...

Use isso com moderação. Se um contêiner depende fortemente de serviços rodando diretamente no host, sua configuração pode se tornar difícil de reproduzir em CI ou na máquina de outro desenvolvedor. Para bancos de dados e caches, rodar a dependência como outro serviço na mesma rede Docker geralmente é mais limpo.

Portas internas devem corresponder ao processo, não ao comentário do Dockerfile

A rede Docker não se importa com o que uma linha EXPOSE do Dockerfile diz, a menos que a ferramenta a use como metadados. A aplicação deve realmente escutar na porta que você chama. Se um aplicativo Node escuta na 3000, outros contêineres devem usar api:3000 mesmo que alguém tenha escrito EXPOSE 8080 por engano.

Verifique também o endereço de bind. Um serviço escutando em 127.0.0.1 dentro de seu contêiner pode não ser alcançável de outros contêineres. Para tráfego entre contêineres, o processo geralmente precisa escutar em 0.0.0.0 ou na interface de rede do contêiner.

Mantenha o design de rede simples

É tentador criar muitas redes porque o recurso existe. Comece com os caminhos de comunicação que você realmente precisa. Um aplicativo pequeno pode precisar apenas da rede padrão do Compose. Um aplicativo web mais realista pode precisar de frontend e backend. Além disso, cada nova rede deve ter uma razão que alguém possa explicar durante um incidente.

Um bom design de rede facilita a solução de problemas. Quando web não consegue alcançar db, e você sabe que eles intencionalmente não compartilham uma rede, a resposta é arquitetural, não misteriosa. Quando todo serviço está conectado a todas as redes, a rede não documenta mais nada.

Uma revisão do mundo real antes de enviar

Antes de considerar um script ou configuração de contêiner finalizada, leia-o uma vez como se você fosse a próxima pessoa que terá que depurá-lo às 2 da manhã. Isso muda o que você percebe. Um prompt que fazia sentido ao escrever o script pode ser ambíguo quando aparece em um log de CI. Um nome de serviço Docker que parecia óbvio pode não corresponder ao nome da variável na aplicação. Um padrão Bash pode ser seguro para desenvolvimento e perigoso para produção.

Gosto de fazer uma simulação curta com valores deliberadamente estranhos. Use um caminho com espaços. Use um valor opcional vazio. Tente um nome de arquivo que comece com um traço. Execute o script de um diretório de trabalho diferente. Inicie o contêiner sem uma variável de ambiente esperada. Esses testes não são sofisticados, mas capturam as suposições que geralmente quebram primeiro.

Verifique também a mensagem de falha. Se a única saída for falhou, o conselho do artigo não chegou à implementação. Uma falha útil diz qual valor foi usado, qual verificação falhou e o que o operador pode mudar. Isso não significa despejar toda variável de ambiente ou imprimir segredos. Significa ser específico onde a especificidade ajuda: o caminho de configuração, o nome do comando ausente, o nome da rede, o nome do host do serviço ou a porta que o processo tentou vincular.

O hábito final é manter exemplos próximos da forma como o sistema é realmente executado. Se a produção usa Compose, teste com Compose. Se um script é iniciado por systemd, teste com systemd ou com um ambiente igualmente mínimo. Se um comando deve ser seguro para copiar e colar, inclua as aspas, separadores -- e validação no próprio exemplo. Leitores copiam padrões funcionais com mais frequência do que copiam avisos.

Essa revisão não é burocracia. É como a automação pequena permanece previsível. Previsível é o que você quer de prompts de shell, carregadores de configuração, expansão de variáveis, diagnósticos de contêiner e rede Docker. Quanto menos surpreendente o comportamento, mais fácil é para o próximo operador confiar nele.

Para rede Docker, documente o caminho de tráfego pretendido ao lado do arquivo Compose ou no README do serviço. Uma nota curta como web -> api:3000 -> db:5432 evita muita confusão. Também facilita revisões: se alguém publicar a porta do banco de dados ou conectar web à rede backend, a mudança tem que se justificar contra o caminho pretendido.

Quando o aplicativo crescer, revise o mapa de rede. Aliases antigos, portas publicadas não utilizadas e serviços conectados a redes que não precisam mais são fontes silenciosas de risco operacional.