Containers são projetados para ser efêmeros. Quando um container é removido, tudo que foi escrito em seu sistema de arquivos desaparece junto. Para uma aplicação web stateless isso é perfeito — mas para um banco de dados, um sistema de arquivos de uploads ou qualquer serviço que precisa persistir dados entre reinicializações, essa característica é um problema.
Da mesma forma, containers são isolados por padrão — um container não enxerga os outros. Uma aplicação Node.js em um container não consegue se conectar a um PostgreSQL em outro container sem que essa comunicação seja explicitamente configurada.
Volumes e redes são as duas funcionalidades do Docker que resolvem esses dois problemas. Entendê-las é essencial para compor sistemas realistas com múltiplos serviços.
Volumes: Persistindo Dados
Um volume é um mecanismo de armazenamento gerenciado pelo Docker que existe independentemente do ciclo de vida dos containers. Quando um container é removido, o volume permanece intacto — e pode ser montado em um novo container.
Existem três formas de montar armazenamento externo em um container:
Volumes gerenciados pelo Docker — o Docker controla onde os dados ficam no host (/var/lib/docker/volumes/). É a forma recomendada para a maioria dos casos.
Bind mounts — mapeia um diretório específico do host para dentro do container. O desenvolvedor controla exatamente onde os dados ficam. Muito usado em desenvolvimento para que mudanças no código local reflitam imediatamente no container.
tmpfs mounts — armazenamento em memória, não persiste em disco. Útil para dados sensíveis temporários.
Trabalhando com Volumes
# Cria um volume gerenciado
docker volume create dados-postgres
# Lista volumes existentes
docker volume ls
# Inspeciona um volume — mostra onde os dados ficam no host
docker volume inspect dados-postgres
# Remove um volume
docker volume rm dados-postgres
# Remove volumes não utilizados por nenhum container
docker volume prune
Montando o volume em um container de banco de dados:
docker run -d \
--name postgres \
-e POSTGRES_USER=app \
-e POSTGRES_PASSWORD=senha123 \
-e POSTGRES_DB=producao \
-v dados-postgres:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
O flag -v dados-postgres:/var/lib/postgresql/data instrui o Docker a montar o volume dados-postgres no caminho /var/lib/postgresql/data dentro do container — que é exatamente onde o PostgreSQL armazena seus arquivos de dados.
Para verificar que a persistência funciona:
# Conecta ao banco e cria uma tabela
docker exec -it postgres psql -U app -d producao -c \
"CREATE TABLE teste (id serial, nome text);"
docker exec -it postgres psql -U app -d producao -c \
"INSERT INTO teste (nome) VALUES ('dado persistido');"
# Remove o container
docker rm -f postgres
# Sobe um novo container com o mesmo volume
docker run -d \
--name postgres-novo \
-e POSTGRES_USER=app \
-e POSTGRES_PASSWORD=senha123 \
-e POSTGRES_DB=producao \
-v dados-postgres:/var/lib/postgresql/data \
postgres:16
# Verifica que os dados ainda existem
docker exec -it postgres-novo psql -U app -d producao -c \
"SELECT * FROM teste;"
A tabela e os dados estarão presentes no novo container — o volume sobreviveu à remoção do container original.
Bind Mounts em Desenvolvimento
Em desenvolvimento, é comum usar bind mounts para que alterações no código local reflitam imediatamente dentro do container, sem precisar reconstruir a imagem:
docker run -d \
--name app-dev \
-v $(pwd)/src:/app/src \
-v $(pwd)/package.json:/app/package.json \
-p 3000:3000 \
minha-app:dev
O $(pwd) captura o diretório atual. O resultado é que /app/src dentro do container aponta diretamente para ./src na máquina local — qualquer arquivo salvo no editor de texto aparece instantaneamente no container.
Em produção, bind mounts devem ser evitados — a imagem deve ser autossuficiente e não depender de arquivos do host.
Redes Docker: Comunicação entre Containers
Por padrão, cada container tem sua própria interface de rede isolada. Para que containers se comuniquem, precisam estar na mesma rede Docker.
O Docker cria automaticamente três redes ao ser instalado:
bridge — a rede padrão. Containers conectados a ela podem se comunicar via IP, mas não por nome.
host — remove o isolamento de rede. O container usa diretamente a interface de rede do host. Útil em casos específicos de performance, mas sacrifica isolamento.
none — sem rede. O container é completamente isolado.
Criando e Usando Redes Customizadas
Redes customizadas são superiores à rede bridge padrão por um motivo fundamental: elas oferecem resolução de nomes automática. Containers na mesma rede customizada podem se referenciar pelo nome do container, sem precisar conhecer IPs.
# Cria uma rede customizada
docker network create rede-app
# Lista redes existentes
docker network ls
# Inspeciona uma rede — mostra containers conectados e configurações
docker network inspect rede-app
# Remove uma rede
docker network rm rede-app
Subindo múltiplos serviços na mesma rede:
# Sobe o banco de dados na rede customizada
docker run -d \
--name postgres \
--network rede-app \
-e POSTGRES_USER=app \
-e POSTGRES_PASSWORD=senha123 \
-e POSTGRES_DB=producao \
-v dados-postgres:/var/lib/postgresql/data \
postgres:16
# Sobe o Redis na mesma rede
docker run -d \
--name redis \
--network rede-app \
redis:7-alpine
# Sobe a aplicação na mesma rede
docker run -d \
--name api \
--network rede-app \
-e DATABASE_URL=postgresql://app:senha123@postgres:5432/producao \
-e REDIS_URL=redis://redis:6379 \
-p 3000:3000 \
minha-app:1.0.0
Observa-se que a variável DATABASE_URL usa postgres como hostname — o nome do container — e não um endereço IP. O Docker resolve esse nome automaticamente dentro da rede rede-app. Da mesma forma, redis como hostname aponta para o container Redis.
Verificando Conectividade entre Containers
Para testar se um container consegue alcançar outro:
# Entra no container da API
docker exec -it api sh
# Testa conectividade com o banco
ping postgres
# Testa a porta do banco
nc -zv postgres 5432
# Testa o Redis
nc -zv redis 6379
exit
Se os containers estão na mesma rede customizada, todos esses testes devem ter sucesso.
Conectando um Container a Múltiplas Redes
Um container pode pertencer a mais de uma rede simultaneamente — útil para separar tráfego de frontend e backend:
# Cria redes separadas
docker network create rede-frontend
docker network create rede-backend
# O container de API pertence às duas redes
docker network connect rede-frontend api
docker network connect rede-backend api
# O banco pertence apenas ao backend
docker network connect rede-backend postgres
# O container de frontend pertence apenas ao frontend
docker run -d \
--name frontend \
--network rede-frontend \
-p 80:80 \
meu-frontend:1.0.0
Nessa configuração, o container frontend não consegue se comunicar diretamente com postgres — só com api. A API faz a ponte entre as duas redes, respeitando o princípio de menor exposição.
Um Ambiente Completo sem Docker Compose
Reunindo volumes e redes em um ambiente com três serviços:
# Cria infraestrutura
docker network create rede-producao
docker volume create dados-postgres
docker volume create dados-redis
# Banco de dados
docker run -d \
--name postgres \
--network rede-producao \
-v dados-postgres:/var/lib/postgresql/data \
-e POSTGRES_USER=app \
-e POSTGRES_PASSWORD=senha_segura \
-e POSTGRES_DB=minha_app \
postgres:16
# Cache
docker run -d \
--name redis \
--network rede-producao \
-v dados-redis:/data \
redis:7-alpine redis-server --appendonly yes
# Aplicação
docker run -d \
--name api \
--network rede-producao \
-e DATABASE_URL=postgresql://app:senha_segura@postgres:5432/minha_app \
-e REDIS_URL=redis://redis:6379 \
-e NODE_ENV=production \
-p 3000:3000 \
minha-app:1.0.0
# Verifica tudo rodando
docker ps
docker network inspect rede-producao
Esse conjunto de comandos funciona, mas já evidencia a necessidade do Docker Compose — gerenciar múltiplos containers manualmente se torna tedioso e propenso a erros rapidamente.
O Que Vem a Seguir
O próximo artigo apresenta o Docker Compose — a ferramenta que descreve em um único arquivo YAML toda a infraestrutura que foi construída com múltiplos comandos neste artigo. O ambiente inteiro sobe com um único docker compose up.
Referências para Aprofundamento
Documentação oficial
- Docker Volumes — docs.docker.com — Guia completo sobre volumes, incluindo drivers de volume para armazenamento em nuvem e NFS.
- Docker Networking — docs.docker.com — Documentação completa sobre redes Docker, cobrindo todos os drivers disponíveis e casos de uso.
Leitura técnica
- Networking in Compose — docs.docker.com — Explica como o Docker Compose gerencia redes automaticamente, complementando o que será visto no próximo artigo.
- Use Bind Mounts — docs.docker.com — Documentação específica sobre bind mounts com boas práticas e diferenças em relação a volumes gerenciados.
Prática
- Play with Docker — O ambiente permite criar múltiplos nós e praticar redes entre containers sem nenhuma instalação local.
Exercícios
Exercício 1
Por que remover um container apaga o que foi escrito dentro dele? O que exatamente o volume muda nessa equação?
Ver resposta
✓ Resposta: Porque o sistema de arquivos do container é uma camada gravável criada sobre a imagem e atrelada ao ciclo de vida daquele container específico — ela nasce e morre com ele. O volume quebra esse vínculo: é gerenciado pelo Docker fora do ciclo de vida do container, fica em /var/lib/docker/volumes/ e sobrevive ao docker rm, podendo ser montado em um container novo. É precisamente isso que o teste do artigo demonstra: cria-se a tabela, destrói-se o container com rm -f, sobe-se outro apontando o mesmo -v, e os dados continuam lá.
Exercício 2
Compare volume gerenciado, bind mount e tmpfs. Em que situação cada um é a escolha certa — e por que bind mount deve ser evitado em produção?
Ver resposta
✓ Resposta: No volume gerenciado o Docker decide onde os dados ficam; é a escolha para dado de produção — banco, uploads — porque não depende da estrutura de diretórios do host e acompanha o container entre máquinas. O bind mount aponta um diretório específico do host, e seu caso clássico é desenvolvimento: o arquivo salvo no editor aparece imediatamente dentro do container, sem rebuild. O tmpfs vive em memória e nunca toca o disco, servindo a dados sensíveis temporários que não devem deixar rastro. O bind mount é ruim em produção justamente pelo que o torna bom em desenvolvimento: ele amarra o container a um caminho existente naquele host específico, o que anula a promessa de rodar em qualquer lugar e faz a imagem deixar de ser autossuficiente.
Exercício 3
Por que o volume do PostgreSQL é montado exatamente em /var/lib/postgresql/data? O que aconteceria se fosse montado em outro caminho?
Ver resposta
✓ Resposta: Porque é ali que o PostgreSQL grava seus arquivos de dados — montar em outro lugar persistiria um diretório que o banco não usa. E o resultado é um erro silencioso, do pior tipo: o container sobe, o banco funciona, os dados são gravados normalmente na camada efêmera, e tudo parece correto. O volume existe, aparece no docker volume ls, e está vazio. A perda só se manifesta no primeiro docker rm — exatamente o momento em que se confiava na persistência. Por isso descobrir o caminho de dados de cada imagem, pela documentação ou por docker inspect, faz parte de usar volumes corretamente.
Exercício 4
Por que a rede bridge padrão não é suficiente para compor serviços? O que a rede customizada acrescenta?
Ver resposta
✓ Resposta: Na bridge padrão os containers se comunicam por IP, mas não por nome. E o IP é atribuído dinamicamente, mudando a cada recriação do container — de modo que qualquer configuração que o referencie deixa de funcionar no próximo restart, sem que nada no código tenha mudado. A rede customizada acrescenta resolução de nomes automática: cada container é alcançável pelo próprio nome dentro daquela rede. É o que faz DATABASE_URL=postgresql://app:senha123@postgres:5432/producao funcionar usando postgres no lugar do IP — e continuar funcionando depois de destruir e recriar o container do banco.
Exercício 5
No exemplo com três serviços, apenas a api recebe -p 3000:3000. O postgres e o redis não publicam porta alguma. Por quê — e qual o risco de publicá-las?
Ver resposta
✓ Resposta: Porque -p serve para expor o serviço para fora, tornando-o acessível a partir do host. A api precisa disso, já que é ela quem recebe as requisições dos usuários. Banco e cache não precisam: eles só são acessados por outros containers da mesma rede, e essa comunicação interna acontece sem publicação nenhuma. Publicar a 5432 sem necessidade coloca o banco ao alcance de qualquer coisa que enxergue a máquina — e em um servidor com IP público, com uma senha fraca em variável de ambiente, isso deixa de ser risco teórico. A regra é publicar apenas a porta de entrada do sistema, nunca as internas.
Exercício 6
O que as redes host e none fazem? O que se ganha e o que se perde ao usar a rede host?
Ver resposta
✓ Resposta: A rede host remove o isolamento de rede: o container passa a usar diretamente a interface do host. Não há mais tradução de portas, e o -p perde o sentido — o que o processo abrir estará aberto na máquina, na mesma porta. Ganha-se algum desempenho, por eliminar uma camada de rede, e perde-se justamente o isolamento, além de reintroduzir o conflito de portas entre serviços que os containers haviam resolvido. A rede none é o extremo oposto: o container fica sem rede alguma, completamente isolado, o que serve a processamento que não deve ter nenhum acesso externo.