Redes e volumes
Como os contêineres se comunicam e o que sobrevive a eles.
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
| Driver | Uso | Observação |
|---|---|---|
bridge | O padrão. Rede privada virtual no host. | O único necessário em 95 % dos casos. |
host | O contêiner compartilha a pilha de rede do host. | Nenhum isolamento, nenhum mapeamento. Somente Linux. |
none | Nenhuma rede, exceto a interface de loopback. | Processamento puramente local. |
macvlan | Endereço MAC próprio na rede física. | Integração de rede específica. |
overlay | Rede 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
| Tipo | Gerenciado por | Localização | Uso principal |
|---|---|---|---|
| Volume nomeado | Docker | /var/lib/docker/volumes/... | Dados de produção: bancos de dados, uploads |
| Bind mount | Você | Qualquer lugar no host | Desenvolvimento: código-fonte, configuração |
| tmpfs | Docker | Somente em memória RAM | Dados 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
bridgepadrã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,
localhostdesigna o próprio contêiner. Para alcançar outro serviço, use o nome dele; para alcançar o host, usehost.docker.internal(com--add-host=host.docker.internal:host-gatewayno Linux). -p 8080:80se associa a todas as interfaces e contorna oufwno Linux. Prefixe com127.0.0.1tudo 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 downos preserva,docker compose down -vos 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 aorootna 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