0%
Redes e volumes no Docker

Redes e volumes

Como os contêineres se comunicam e o que sobrevive a eles.

10-15 min

Redes e volumes no Docker

Duas perguntas aparecem invariavelmente assim que se vai além do contêiner isolado, e elas sozinhas produzem a maioria das mensagens de erro incompreensíveis dos primeiros meses.

A primeira: “por que minha aplicação não consegue alcançar o banco de dados?” Você iniciou os dois contêineres, cada um está rodando, o docker ps está todo verde, e a API responde obstinadamente connection refused em localhost:5432. O reflexo é procurar um endereço IP, fixá-lo no código, constatar que funciona — até a próxima reinicialização, quando o IP mudou. O problema nunca foi o endereço: é que localhost, dentro de um contêiner, não designa a mesma máquina que no host.

A segunda: “por que meus dados sumiram?” Um banco Postgres preenchido durante dois dias de testes, um docker compose down -v digitado sem pensar, e tudo foi embora. Não é um acidente: é o comportamento documentado. Um contêiner é efêmero por concepção, e sua camada de escrita morre com ele. A persistência nunca é implícita, ela se declara.

Essas duas perguntas têm a mesma raiz: um contêiner é isolado por padrão, tanto na rede quanto no armazenamento. Esse isolamento é o que torna o Docker reprodutível; as redes e os volumes são os dois mecanismos explícitos que o perfuram, exatamente onde você decide.

Os drivers de rede e como escolher

DriverUsoObservação
bridgeO padrão. Rede privada virtual no host.O único necessário em 95 % dos casos.
hostO contêiner compartilha a pilha de rede do host.Nenhum isolamento, nenhum mapeamento. Somente Linux.
noneNenhuma rede, exceto a interface de loopback.Processamento puramente local.
macvlanEndereço MAC próprio na rede física.Integração de rede específica.
overlayRede distribuída entre vários hosts.Reservado a clusters Swarm.
# Les reseaux existants. bridge, host et none sont crees a l'installation.
docker network ls

# Le detail : plage d'adresses, passerelle, conteneurs connectes
docker network inspect bridge

O driver host economiza alguns microssegundos de latência ao eliminar a tradução de endereços, ao custo de todo o isolamento: o contêiner ocupa diretamente as portas do host, com as colisões que isso implica. É um recurso de exceção, não uma otimização padrão. E só vale no Linux: no macOS e no Windows, a pilha “host” seria a da VM intermediária.

O ponto que destrava tudo: bridge padrão versus bridge personalizada

Esta é a diferença mais útil desta lição, e quase sempre é ignorada.

Todo contêiner iniciado sem qualquer especificação entra na rede bridge padrão. Nessa rede, a resolução DNS por nome de contêiner não funciona.

docker run -d --name serveur nginx:1.27-alpine
docker run --rm alpine:3.20 ping -c 2 serveur
# ping: bad address 'serveur'   ->  echec attendu

Crie você mesmo uma rede, e o comportamento muda completamente: o Docker anexa a ela um servidor DNS interno que resolve nomes de contêineres.

docker network create app-net

docker run -d --name serveur --network app-net nginx:1.27-alpine
docker run --rm --network app-net alpine:3.20 ping -c 2 serveur
# 2 packets transmitted, 2 packets received  ->  succes

# La requete HTTP passe sur le port INTERNE (80), sans qu'aucun
# port n'ait ete publie sur l'hote.
docker run --rm --network app-net alpine:3.20 \
  sh -c "apk add --no-cache curl >/dev/null && curl -s -o /dev/null -w '%{http_code}\n' http://serveur"

A regra a lembrar: crie sempre uma rede nomeada para suas aplicações. É o que o Docker Compose faz automaticamente — a rede <projeto>_default — e é por isso que, na lição anterior, a API alcançava seu banco no endereço db:5432 sem nenhuma configuração. Isso resolve de quebra o problema dos endereços IP: o DNS interno acompanha os contêineres, e um serviço recriado com um novo IP continua acessível pelo mesmo nome, imediatamente.

Um contêiner pode ser conectado a várias redes, a quente:

docker network create back-net
docker network connect back-net serveur
docker network disconnect back-net serveur

docker inspect serveur --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}'

Publicar uma porta: o que o -p faz exatamente

O -p 8080:80 não abre a porta 80 do contêiner — ela já estava aberta dentro da rede dele. O que o -p faz é instalar uma regra de tradução de endereços no host. A ordem host:contêiner se memoriza pelo sentido do tráfego: vem-se do host, vai-se em direção ao contêiner.

# Port 8080 de l'hote -> port 80 du conteneur
docker run -d -p 8080:80 nginx:1.27-alpine

# Se lier uniquement a l'interface de bouclage : accessible depuis
# la machine, INVISIBLE depuis le reseau local.
docker run -d -p 127.0.0.1:8080:80 nginx:1.27-alpine

# Laisser Docker choisir un port libre sur l'hote
docker run -d -p 80 nginx:1.27-alpine

# Publier tous les ports declares par EXPOSE
docker run -d -P nginx:1.27-alpine

# Voir les correspondances effectives
docker port <conteneur>

A segunda forma merece uma ênfase particular, porque corrige um problema de segurança que muita gente descobre tarde demais. O -p 8080:80 se vincula por padrão a 0.0.0.0, ou seja, a todas as interfaces, inclusive o endereço público. E no Linux, o Docker grava suas regras de tradução em uma cadeia do iptables avaliada antes das de um firewall de aplicação como o ufw: uma porta publicada pelo Docker pode, portanto, continuar acessível pela Internet mesmo quando o ufw status afirma bloqueá-la. Não é um bug, é a ordem das cadeias de filtragem — mas a surpresa é desagradável quando o banco em questão não tinha senha.

A boa prática cabe em uma frase: publique apenas o que precisa ser acessível do exterior, e prefixe com 127.0.0.1 tudo o que só precisa ser acessível localmente.

services:
  web:
    ports:
      - "80:8080"              # public : c'est voulu
  db:
    ports:
      - "127.0.0.1:5432:5432"  # local uniquement, pour un client graphique
  cache:
    expose:
      - "6379"                 # documentation seulement, rien de publie

Uma observação sobre o localhost, que está na origem do primeiro problema apresentado na introdução. Dentro de um contêiner, localhost designa o próprio contêiner. Uma aplicação que procura seu banco em localhost:5432 está, portanto, procurando um Postgres dentro de si mesma; o endereço correto é o nome do serviço, db:5432. E se um contêiner realmente precisar alcançar um serviço que roda no host, existe um nome dedicado:

# Sur Docker Desktop (macOS, Windows), ce nom est fourni d'office
docker run --rm alpine:3.20 ping -c 1 host.docker.internal

# Sur Linux, il faut le declarer explicitement
docker run --rm --add-host=host.docker.internal:host-gateway \
  alpine:3.20 ping -c 1 host.docker.internal

Segmentar a pilha pela rede

Como um contêiner só alcança as redes às quais pertence, as redes são uma verdadeira ferramenta de compartimentação. O padrão clássico separa uma camada exposta de uma camada interna.

services:
  proxy:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
    networks:
      - frontend            # seul service joignable de l'exterieur

  api:
    build: .
    networks:
      - frontend            # joignable par le proxy
      - backend             # peut joindre la base

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db-data:/var/lib/postgresql/data
    networks:
      - backend             # UNIQUEMENT le reseau interne

networks:
  frontend:
  backend:
    internal: true          # aucune route vers l'exterieur

volumes:
  db-data:

O interesse não é teórico. O proxy não pertence à rede backend: mesmo que seja comprometido, ele não consegue abrir uma conexão TCP para o db, porque nenhuma rota existe. A opção internal: true vai além ao suprimir a rota padrão para o exterior, o que limita fortemente a exfiltração de dados a partir dessa rede.

docker compose exec proxy sh -c "nc -zv db 5432"   # doit echouer
docker compose exec api sh -c "nc -zv db 5432"     # doit reussir

Três formas de persistir e quando usar cada uma

TipoGerenciado porLocalizaçãoUso principal
Volume nomeadoDocker/var/lib/docker/volumes/...Dados de produção: bancos de dados, uploads
Bind mountVocêQualquer lugar no hostDesenvolvimento: código-fonte, configuração
tmpfsDockerSomente em memória RAMDados sensíveis ou temporários

O volume nomeado é um espaço que o Docker cria e gerencia. Você não conhece seu caminho real, e é justamente essa a vantagem: ele é portável, independente da árvore de diretórios do host, pode ser copiado por ferramentas do Docker e é idêntico nos três sistemas.

O bind mount monta uma pasta existente do host no contêiner. Perfeito para o desenvolvimento — você modifica um arquivo e o contêiner o enxerga instantaneamente — e inadequado para a produção, pois acopla a aplicação à árvore de diretórios específica de uma determinada máquina.

# Volume nomme : Docker gere l'emplacement
docker run -d --name pg1 \
  -e POSTGRES_PASSWORD=secret \
  -v pg-data:/var/lib/postgresql/data \
  postgres:16-alpine

# Bind mount : chemin ABSOLU de l'hote, obligatoirement
docker run -d --name web1 \
  -v "$(pwd)/site:/usr/share/nginx/html:ro" \
  -p 8080:80 nginx:1.27-alpine

# tmpfs : en memoire, disparait a l'arret
docker run -d --name calcul --tmpfs /tmp:rw,size=64m alpine:3.20 sleep 3600

A distinção se resume a um caractere: -v pg-data:/caminho sem barra inicial designa um volume nomeado, -v /caminho/host:/caminho um bind mount. Um erro de digitação faz, portanto, alternar silenciosamente de um mecanismo para o outro. A sintaxe --mount, mais verbosa, elimina a ambiguidade e falha de forma limpa se a origem não existir:

docker run -d \
  --mount type=volume,source=pg-data,target=/var/lib/postgresql/data \
  --mount type=bind,source="$(pwd)/conf",target=/etc/app,readonly \
  --mount type=tmpfs,target=/tmp,tmpfs-size=64m \
  -e POSTGRES_PASSWORD=secret \
  postgres:16-alpine

Existe um comportamento diferenciador que é preciso conhecer, pois ele explica situações desconcertantes. Quando um volume nomeado vazio é montado sobre uma pasta não vazia da imagem, o Docker copia o conteúdo da imagem para o volume no primeiro uso. Já um bind mount não copia nada: ele simplesmente oculta o conteúdo da imagem.

# Le volume est pre-rempli avec la page par defaut de nginx
docker run -d --name v1 -v html-vol:/usr/share/nginx/html nginx:1.27-alpine
docker run --rm -v html-vol:/data alpine:3.20 ls /data   # index.html present

# Le bind mount d'un dossier vide masque tout : nginx renvoie 403
mkdir -p ./vide
docker run -d --name v2 -p 8081:80 \
  -v "$(pwd)/vide:/usr/share/nginx/html" nginx:1.27-alpine

É exatamente por essa razão que o arquivo de sobrescrita da lição anterior adicionava um volume anônimo - /app/node_modules: o bind mount de ./src não podia ocultar as dependências instaladas na imagem.

Gerenciar os volumes no dia a dia

# Creer explicitement (facultatif : un volume inconnu est cree a la volee)
docker volume create pg-data

# Lister. Les noms en hexadecimal sont des volumes ANONYMES.
docker volume ls

# Ou vivent reellement les donnees, et qui les utilise
docker volume inspect pg-data

# Supprimer. Refuse si un conteneur l'utilise.
docker volume rm pg-data

# Supprimer tous les volumes inutilises. COMMANDE DESTRUCTRICE :
# listez d'abord, decidez ensuite.
docker volume ls -f dangling=true
docker volume prune

Dois pontos de atenção. Os volumes sobrevivem ao docker rm: remover um contêiner não remove seus dados, o que é intencional, mas explica o acúmulo silencioso de volumes órfãos. E os volumes anônimos se empilham com identificadores ilegíveis: nomeie sistematicamente os seus.

Para inspecionar um volume sem iniciar o serviço que o utiliza, o contêiner descartável é a ferramenta ideal:

docker run --rm -v pg-data:/data alpine:3.20 ls -la /data
docker run --rm -v pg-data:/data alpine:3.20 du -sh /data

O problema das permissões nos bind mounts

No Linux, uma armadilha espera todo mundo uma vez. As permissões se baseiam em identificadores numéricos (UID, GID), e esses números não são traduzidos entre o host e o contêiner. Se o contêiner escreve como usuário 1001 em uma pasta montada do host, os arquivos criados pertencem ao UID 1001 do host — uma conta que talvez nem exista. O sintoma habitual: arquivos pertencentes ao root aparecem na sua pasta de trabalho, ou a aplicação reclama de permission denied em uma pasta que você enxerga como acessível.

# Reproduction : le conteneur ecrit en root
docker run --rm -v "$(pwd):/work" -w /work alpine:3.20 touch fichier-root
ls -l fichier-root     # appartient a root

# Solution : imposer l'UID et le GID de l'utilisateur courant
docker run --rm --user "$(id -u):$(id -g)" \
  -v "$(pwd):/work" -w /work alpine:3.20 touch fichier-moi
ls -l fichier-moi      # vous appartient
# Dans Compose, avec une variable pour rester portable
services:
  api:
    build: .
    user: "${UID:-1000}:${GID:-1000}"
    volumes:
      - ./src:/app/src

Esse problema não existe no macOS e no Windows, onde a camada de compartilhamento de arquivos do Docker Desktop reescreve os proprietários. Em contrapartida, essas plataformas sofrem com um custo de desempenho nos arquivos compartilhados: uma pasta node_modules ou .git em bind mount pode deixar um projeto uma ordem de grandeza mais lento. A solução é manter essas pastas em um volume Docker e montar apenas o código-fonte.

Um último reflexo útil: monte em somente leitura tudo o que o contêiner não precisa modificar — configuração, conjuntos de dados, certificados. É gratuito e elimina uma classe inteira de acidentes.

docker run -d -v "$(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro" nginx:1.27-alpine

Fazer backup e restaurar um volume

Um volume nomeado não se copia com cp: sua localização pertence ao Docker e, no macOS ou no Windows, ele fica dentro da VM. O método portável consiste em montá-lo em um contêiner descartável que grava um arquivo compactado em uma pasta do host.

# --- Sauvegarde : le volume en lecture, un dossier de l'hote en ecriture
docker run --rm \
  -v pg-data:/data:ro \
  -v "$(pwd):/sauvegarde" \
  alpine:3.20 \
  tar czf /sauvegarde/pg-data-$(date +%F).tar.gz -C /data .

# --- Restauration
docker volume create pg-data-restaure
docker run --rm \
  -v pg-data-restaure:/data \
  -v "$(pwd):/sauvegarde" \
  alpine:3.20 \
  tar xzf /sauvegarde/pg-data-2026-01-15.tar.gz -C /data

Uma ressalva importante para bancos de dados: copiar os arquivos de um motor em plena escrita pode produzir um arquivo incoerente. Pare o contêiner antes da cópia ou — melhor ainda — use a ferramenta de backup lógico do motor, que garante um estado consistente:

# Backup lógico do Postgres, com o motor em funcionamento
docker compose exec -T db pg_dump -U app -d appdb | gzip > appdb-$(date +%F).sql.gz

# Restauração
gunzip -c appdb-2026-01-15.sql.gz | docker compose exec -T db psql -U app -d appdb

O -T desativa a alocação de um pseudo-terminal, indispensável sempre que se redireciona um fluxo: sem ele, o arquivo produzido fica corrompido pelos caracteres de controle do terminal.

Redes e volumes no Compose

O Compose declara os dois no nível superior do arquivo, o que dá uma visão completa da topologia em um único lugar. A forma longa é mais verbosa, mas bem mais legível em uma revisão.

services:
  api:
    build: .
    networks:
      - backend
    volumes:
      - uploads:/app/uploads          # forma curta : volume nomeado
      - type: bind
        source: ./config
        target: /app/config
        read_only: true
      - type: tmpfs
        target: /tmp
        tmpfs:
          size: 67108864              # 64 MB, em bytes

networks:
  backend:
    driver: bridge

volumes:
  uploads:

Lembrete da armadilha de nomenclatura vista na lição anterior: o Compose prefixa os volumes com o nome do projeto, então uploads vira monprojet_uploads. Renomear a pasta do projeto cria um novo volume vazio e dá a impressão de que os dados desapareceram, quando na verdade eles continuam guardados sob o prefixo antigo.

docker volume ls | grep uploads       # encontrar um volume por trás do seu prefixo
docker compose -p monprojet up -d     # fixar o prefixo para não depender mais dele

Pontos-chave

  • Na rede bridge padrão, a resolução por nome não funciona. Crie sempre uma rede nomeada — o que o Compose faz por você — e conecte seus serviços pelo nome, nunca por um endereço IP que vai mudar.
  • Dentro de um contêiner, localhost designa o próprio contêiner. Para alcançar outro serviço, use o nome dele; para alcançar o host, use host.docker.internal (com --add-host=host.docker.internal:host-gateway no Linux).
  • -p 8080:80 se associa a todas as interfaces e contorna o ufw no Linux. Prefixe com 127.0.0.1 tudo o que não precisa ser público, e não publique um banco de dados por hábito.
  • Volume nomeado para dados que precisam durar, bind mount para o código em desenvolvimento, tmpfs para o que nunca deve tocar o disco. Um volume nomeado vazio herda o conteúdo da imagem; um bind mount o oculta.
  • Os volumes sobrevivem à remoção dos contêineres. docker compose down os preserva, docker compose down -v os destrói: a diferença entre os dois são os seus dados.
  • No Linux, os UIDs não são traduzidos através de um bind mount. --user "$(id -u):$(id -g)" evita arquivos pertencentes ao root na sua pasta de trabalho.
  • Faça backup de um volume por meio de um contêiner descartável que produza um arquivo compactado; para um banco de dados, prefira sempre uma exportação lógica (pg_dump) a uma cópia de arquivos a quente.

Dica: em caso de dúvida sobre uma conexão entre contêineres, docker network inspect <reseau> exibe a lista exata dos contêineres conectados. Nove em cada dez vezes, o serviço que “não responde” simplesmente não está na mesma rede.

Comentários

Os comentários são alimentados pelo GitHub Discussions

Conecte-se com o GitHub para participar da discussão

Link copiado!