Docker Compose é muito mais do que uma ferramenta para subir um container de banco de dados e um de aplicação localmente. Para equipes DevOps e administradores de infraestrutura, dominar o docker compose avançado é essencial para criar ambientes de orquestração containers eficientes, seguros e escaláveis. Neste tutorial, vamos além do básico docker-compose.yml simples. Vamos explorar como configurar variáveis de ambiente, redes personalizadas, volumes nominais, dependências condicionais e integração com sistemas de monitoramento.
1. Estrutura do Projeto e Organização
A orquestração eficiente começa com a organização. Em vez de manter um único arquivo monolítico, a prática recomendada para microserviços é separar responsabilidades. Vamos estruturar nosso projeto para uma aplicação web típica composta por um Frontend (Nginx), um Backend (Node.js) e um Banco de Dados (PostgreSQL).
Crie a seguinte estrutura de diretórios:
./-
docker-compose.yml(Arquivo principal) -
.env(Variáveis de ambiente sensíveis) -
backend/ -
Dockerfile -
server.js -
frontend/ -
Dockerfile -
index.html -
db/ -
init.sql
Essa separação permite que cada serviço tenha seu próprio contexto de build e ciclo de vida, facilitando a manutenção e o deploy automatizado em pipelines CI/CD.
2. Configuração do Arquivo Principal (docker-compose.yml)
O coração da orquestração está no arquivo docker-compose.yml. Vamos definir as versões, os serviços e suas interconexões. Utilizaremos a versão 3.8 ou superior para garantir suporte a recursos modernos como deploy replicas e healthchecks avançados.
version: '3.8'
services:
db:
image: postgres:15-alpine
container_name: app_db
restart: always
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: ${DB_NAME}
volumes:
- db_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- backend_net
backend:
build: ./backend
container_name: app_backend
restart: on-failure
environment:
NODE_ENV: production
DB_HOST: db
DB_PORT: 5432
DB_USER: ${DB_USER}
DB_PASS: ${DB_PASS}
DB_NAME: ${DB_NAME}
depends_on:
db:
condition: service_healthy
networks:
- backend_net
ports:
- "3000:3000"
frontend:
build: ./frontend
container_name: app_frontend
restart: always
depends_on:
- backend
ports:
- "80:80"
networks:
- frontend_net
- backend_net
volumes:
db_data:
driver: local
networks:
frontend_net:
driver: bridge
backend_net:
driver: bridge
Neste exemplo, note a utilização de variáveis de ambiente (${DB_USER}) para não hardcodar credenciais. A seção volumes define um volume nomial db_data, garantindo que os dados persistam mesmo se o container for removido. As redes separadas (frontend_net e backend_net) criam uma segmentação lógica, onde o frontend pode falar com o backend, mas não diretamente com o banco de dados, aumentando a segurança.
3. Segurança: Gerenciamento de Variáveis de Ambiente
A orquestração containers exige cuidado redobrado com segredos. Nunca coloque senhas no código fonte ou no arquivo docker-compose.yml. Utilize um arquivo .env na raiz do projeto.
# .env
DB_USER=admin
DB_PASS=supersecret123
DB_NAME=myapp_db
O Docker Compose lê automaticamente este arquivo quando você executa os comandos de up. Para ambientes de produção, considere usar Docker Secrets ou integrar com ferramentas como HashiCorp Vault para rotacionar credenciais dinamicamente.
4. Volumes e Persistência de Dados
A persistência é crítica em infraestruturas reais. No exemplo acima, utilizamos um volume nomial db_data. Isso significa que o Docker gerencia o armazenamento no host (geralmente em /var/lib/docker/volumes/).
Para casos onde você precisa mapear diretórios específicos do host ou usar sistemas de arquivos distribuídos (como NFS ou AWS EBS), você pode usar volumes bind:
volumes:
- /host/path/to/data:/var/lib/postgresql/data
No entanto, para portabilidade e abstração da infraestrutura subjacente, volumes nominais definidos na seção volumes do compose são preferíveis. Eles permitem que você execute o mesmo stack em máquinas diferentes sem alterar os caminhos absolutos.
5. Redes Personalizadas e Segmentação
O Docker cria uma rede padrão de bridge para cada projeto, mas redes customizadas oferecem DNS interno e isolamento melhorado. No nosso exemplo, definimos backend_net e frontend_net.
O serviço backend está conectado apenas à backend_net, enquanto o frontend está em ambas. Isso permite que o frontend acesse o backend via nome de host (ex: http://backend:3000) e que o backend acesse o banco de dados, mas bloqueia tentativas diretas do frontend ao banco de dados.
Para verificar as redes e suas configurações, utilize:
docker network ls
docker network inspect backend_net
6. Dependências Condicionais e Healthchecks
Um erro comum em orquestração é iniciar o serviço de aplicação antes que o banco de dados esteja pronto para receber conexões. A diretiva depends_on tradicional apenas aguarda o container do banco iniciar, não se ele está funcional.
Para resolver isso, implemente um healthcheck. Vamos atualizar a configuração do serviço db:
db:
image: postgres:15-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 10s
timeout: 5s
retries: 5
Agora, atualize a dependência no serviço backend:
backend:
depends_on:
db:
condition: service_healthy
Com essa configuração, o Docker Compose aguardará até que o script pg_isready retorne sucesso (indicando que o PostgreSQL está aceitando conexões) antes de iniciar o backend. Isso elimina erros de "Connection refused" durante o deploy.
7. Otimização de Builds com Dockerfiles Eficientes
A infraestrutura eficiente depende de imagens leves. No diretório backend/, um Dockerfile otimizado para Node.js em produção deve usar multi-stage builds:
# backend/Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/server.js .
EXPOSE 3000
CMD ["node", "server.js"]
Este Dockerfile primeiro instala as dependências e depois copia apenas os arquivos necessários para a imagem final. Isso resulta em imagens menores, mais rápidas de transferir durante o deploy e com menor superfície de ataque.
8. Comandos Essenciais para Gerenciamento
Com o ambiente configurado, aqui estão os comandos fundamentais para operar sua orquestração:
- Iniciar o stack:
docker-compose up -d --build
O flag -d executa em modo detached (background), e --build garante que mudanças no Dockerfile sejam refletidas.
- Verificar logs agregados:
docker-compose logs -f backend
O flag -f segue os logs em tempo real, essencial para debugging.
- Parar e remover recursos:
docker-compose down -v
O flag -v remove também os volumes nominais. Use com cautela em produção, pois isso apaga dados persistentes.
- Executar comandos temporários:
docker-compose exec backend npm test
Isso permite que você execute tarefas administrativas dentro do contexto de um container específico sem precisar entrar em uma shell interativa.
9. Automação e Integração com CI/CD
A orquestração containers ganha seu verdadeiro valor quando integrada a pipelines de automação. Em sistemas como GitLab CI, GitHub Actions ou Jenkins, o processo típico envolve:
- Construir as imagens Docker.
- Enviar (push) para um registry (Docker Hub, ECR, GCR).
- Atualizar o
docker-compose.ymlcom a nova tag da imagem. - Executar
docker-compose pull && docker-compose up -d.
Exemplo de script bash para deploy automatizado:
#!/bin/bash
# Atualiza as imagens
docker-compose pull
# Reinicia os serviços com novas versões
docker-compose up -d --no-deps backend frontend
O flag --no-deps é útil para reiniciar apenas os serviços específicos que foram atualizados, minimizando o downtime e o impacto no banco de dados.
10. Boas Práticas Finais para Infraestrutura
- Rename Containers: Sempre defina
container_namepara facilitar a identificação e o gerenciamento manual se necessário. - Resource Limits: Em ambientes de produção, defina limites de CPU e memória para evitar que um microserviço consuma todos os recursos do host. Isso requer configuração no daemon do Docker ou uso de Swarm/Kubernetes, mas é uma consideração vital de infraestrutura.
- Logging Drivers: Configure drivers de log centralizados (como
json-filecom rotação oufluentd) para garantir que logs sejam preservados e analisáveis externamente.
Dominar o Docker Compose avançado transforma a gestão de containers de uma tarefa operacional reativa em um processo proativo e escalável. Ao aplicar essas práticas de orquestração, você garante maior estabilidade, segurança e eficiência para suas aplicações de microserviços.