Docker Volumes e Redes: Persistência e Comunicação

[95] Docker Volumes e Redes: Persistência e Comunicação

Volumes e redes no Docker: por que o sistema de arquivos do container é efêmero, a diferença entre volumes gerenciados, bind mounts e tmpfs, a resolução de nomes que só as redes customizadas oferecem, e a composição de múltiplos serviços que se comunicam entre si.
DevOps

11 min de leitura

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

Leitura técnica

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.

Comentários

Mais em DevOps

Permissões, Usuários e Grupos no Linux
Permissões, Usuários e Grupos no Linux

O sistema de permissões do Linux como mecanismo de isolamento entre serviços…

Instalando e Rodando Seus Primeiros Containers
Instalando e Rodando Seus Primeiros Containers

Docker na prática: instalação pelo repositório oficial, uso sem sudo…

Trabalhando em Equipe com Git Flow
Trabalhando em Equipe com Git Flow

Git Flow como modelo de organização de branches: os papéis de main e develop…