Docker Compose na prática: app, banco e cache
1. Visão geral da aplicação multicontainer
Em cenários reais de desenvolvimento, raramente temos uma aplicação isolada. Uma API web típica consome um banco de dados relacional e utiliza um cache para melhorar performance. Gerenciar manualmente cada container com docker run torna-se rapidamente inviável.
Vamos construir um stack composto por:
- App: API em Node.js (Express) que expõe endpoints REST
- Banco: PostgreSQL para persistência de dados estruturados
- Cache: Redis para armazenamento temporário e sessões
O Docker Compose resolve o problema de orquestração local permitindo definir, configurar e iniciar todos os serviços com um único comando. Cada serviço roda em seu próprio container, mas compartilha uma rede interna que possibilita comunicação por nome do serviço.
2. Estrutura do projeto e Dockerfile
A organização do projeto segue boas práticas de separação de responsabilidades:
meu-projeto/
├── app/
│ ├── Dockerfile
│ ├── package.json
│ └── src/
│ ├── index.js
│ ├── database.js
│ └── cache.js
├── docker-compose.yml
├── docker-compose.override.yml
└── .env
O Dockerfile da aplicação utiliza multi-stage build para otimizar o tamanho da imagem em produção:
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY src/ ./src/
EXPOSE 3000
CMD ["node", "src/index.js"]
Variáveis de ambiente como DB_HOST, DB_PASSWORD e REDIS_HOST são injetadas em tempo de execução pelo Compose, mantendo a imagem genérica e reutilizável.
3. Definindo os serviços no docker-compose.yml
O arquivo principal docker-compose.yml declara os três serviços com suas configurações específicas:
version: '3.8'
services:
app:
build:
context: ./app
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- DB_HOST=db
- DB_PORT=5432
- DB_USER=app_user
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=appdb
- REDIS_HOST=cache
- REDIS_PORT=6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
db:
image: postgres:15-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
- POSTGRES_USER=app_user
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=appdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d appdb"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pgdata:
Observe que apenas a porta da aplicação (3000) é exposta para o host. O Redis tem porta exposta para facilitar debug, mas em produção isso seria removido.
4. Redes e comunicação entre containers
O Docker Compose cria automaticamente uma rede bridge para o projeto. Cada container pode acessar os outros pelo nome do serviço definido no YAML.
A comunicação funciona da seguinte forma:
- O serviço app conecta-se ao banco usando o hostname db e porta 5432
- O cache é acessado via cache:6379
- Nenhum serviço precisa conhecer IPs ou configurações de rede manualmente
Para segurança, apenas a porta da aplicação fica acessível externamente. Banco e cache permanecem isolados na rede interna, reduzindo a superfície de ataque.
5. Volumes e persistência de dados
Cada tipo de dado recebe tratamento adequado quanto à persistência:
Volume nomeado para PostgreSQL (pgdata): Garante que os dados do banco sobrevivam a paradas e reinicializações dos containers. Mesmo com docker-compose down, os dados permanecem.
Redis sem volume persistente: O cache Redis é volátil por natureza. Se o container reiniciar, os dados em cache são perdidos, o que é aceitável e até desejável em muitos cenários.
Bind mount para desenvolvimento: No arquivo de override de desenvolvimento, adicionamos:
services:
app:
volumes:
- ./app/src:/app/src
Isso permite hot reload da aplicação sem precisar reconstruir a imagem a cada alteração no código.
6. Healthchecks e dependências entre serviços
Inicialização ordenada é crítica para aplicações multicontainer. O PostgreSQL pode levar alguns segundos para ficar pronto, e a aplicação não deve tentar conectar antes disso.
Configuramos healthchecks no banco e no cache:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d appdb"]
interval: 5s
timeout: 5s
retries: 5
O depends_on com condition: service_healthy faz o Compose aguardar até que o serviço dependente esteja saudável antes de iniciar a aplicação.
Para cenários mais complexos, podemos incluir um script wait-for-it.sh no entrypoint da aplicação:
COPY wait-for-it.sh /wait-for-it.sh
RUN chmod +x /wait-for-it.sh
CMD ["/wait-for-it.sh", "db:5432", "--", "node", "src/index.js"]
7. Docker Compose para desenvolvimento vs produção
Utilizamos arquivos separados para cada ambiente:
docker-compose.yml (base): Contém a configuração comum que funciona em ambos os ambientes.
docker-compose.override.yml (desenvolvimento): Automaticamente aplicado pelo Compose quando executamos docker-compose up. Inclui bind mounts, variáveis de debug e serviços auxiliares.
docker-compose.prod.yml (produção): Usado explicitamente com -f docker-compose.yml -f docker-compose.prod.yml. Remove exposição de portas desnecessárias e adiciona políticas de restart.
Perfis permitem ativar serviços opcionais sob demanda:
services:
adminer:
image: adminer
profiles: ["tools"]
ports:
- "8080:8080"
depends_on:
- db
Ativamos com: docker-compose --profile tools up
8. Comandos práticos e troubleshooting
Comandos essenciais para o dia a dia:
# Iniciar todos os serviços
docker-compose up -d
# Ver logs de todos os serviços em tempo real
docker-compose logs -f
# Ver logs de um serviço específico
docker-compose logs -f app
# Executar comando dentro de um container
docker-compose exec app sh
# Listar serviços e seus status
docker-compose ps
# Parar e remover containers, redes e volumes
docker-compose down -v
# Reconstruir imagens sem usar cache
docker-compose build --no-cache
Para resetar dados de desenvolvimento:
docker-compose down -v
docker-compose up -d
Isso remove todos os volumes, incluindo o banco de dados, e recria tudo do zero.
Inspecionar logs simultâneos ajuda a identificar problemas de comunicação entre serviços. Se a aplicação não consegue conectar ao banco, os logs mostrarão erros de conexão, indicando possível problema de healthcheck ou configuração de ambiente.
Referências
- Documentação oficial do Docker Compose — Guia completo sobre definição e gerenciamento de aplicações multicontainer com Compose
- Multi-stage builds no Docker — Tutorial oficial sobre otimização de imagens com builds em múltiplos estágios
- Healthcheck instructions no Dockerfile — Referência sobre configuração de healthchecks para containers
- PostgreSQL Docker image oficial — Documentação da imagem oficial com variáveis de ambiente e healthcheck
- Redis Docker image oficial — Guia de uso da imagem Redis com opções de configuração por command-line
- Using Docker Compose profiles — Como utilizar perfis para gerenciar serviços opcionais em diferentes ambientes