Docker Compose: orquestrando múltiplos containers

1. Introdução ao Docker Compose no contexto DevOps

1.1. O que é Docker Compose e por que ele é essencial para orquestração local

Docker Compose é uma ferramenta que permite definir e executar aplicações multi-container com um único arquivo YAML. No contexto DevOps, ele se torna indispensável para reproduzir ambientes complexos localmente, garantindo que desenvolvedores, testadores e pipelines de CI/CD utilizem exatamente a mesma configuração de serviços, redes e volumes. Com um simples docker-compose up, toda a infraestrutura da aplicação é levantada de forma consistente.

1.2. Diferença entre Docker Compose e orquestradores completos

Enquanto Docker Compose é ideal para ambientes de desenvolvimento e testes locais, orquestradores como Kubernetes e Docker Swarm são projetados para produção em larga escala. Compose opera em um único host e não oferece recursos como auto-scaling, rolling updates avançados ou balanceamento de carga distribuído. Já Kubernetes gerencia clusters multi-host com alta disponibilidade, mas com complexidade operacional muito maior.

1.3. Casos de uso típicos

  • Ambientes de desenvolvimento: equipes podem subir stacks completas (app + banco + cache + fila) em segundos.
  • Testes integrados: pipelines CI sobem serviços dependentes para execução de testes de integração.
  • Demonstrações e prototipagem: compartilhar um docker-compose.yml permite que qualquer pessoa reproduza o ambiente.

2. Estrutura fundamental do arquivo docker-compose.yml

2.1. Anatomia de um Compose file

version: '3.8'

services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"

  db:
    image: postgres:15
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: secret

volumes:
  pgdata:

networks:
  default:
    driver: bridge

A estrutura básica contém: version (define a sintaxe), services (containers), networks (comunicação) e volumes (persistência).

2.2. Definindo serviços

Cada serviço pode ser configurado com:

services:
  app:
    build: ./app          # Dockerfile local
    image: myapp:latest   # ou imagem pré-existente
    ports:
      - "3000:3000"
    environment:
      - DB_HOST=db
      - DB_PORT=5432
    depends_on:
      - db
    volumes:
      - ./app:/usr/src/app

2.3. Boas práticas de versionamento

  • Sempre especificar a versão do Compose (atualmente 3.8 ou 3.9).
  • Manter o YAML indentado corretamente (2 espaços).
  • Usar variáveis de ambiente para valores sensíveis.
  • Versionar o docker-compose.yml junto com o código da aplicação.

3. Gerenciamento de dependências e ordem de inicialização

3.1. depends_on básico

services:
  app:
    depends_on:
      - db
      - redis

Isso garante que db e redis iniciem antes de app, mas não espera que estejam prontos para receber conexões.

3.2. Healthchecks customizados

services:
  db:
    image: postgres:15
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  app:
    depends_on:
      db:
        condition: service_healthy

Com condition: service_healthy, o serviço app só inicia quando o PostgreSQL realmente estiver aceitando conexões.

3.3. Estratégias de retry com wait-for-it

Para serviços que não suportam healthcheck nativo, use scripts como wait-for-it.sh no entrypoint:

services:
  app:
    entrypoint: ["./wait-for-it.sh", "db:5432", "--", "npm", "start"]

4. Redes e comunicação entre containers no Compose

4.1. Rede padrão e DNS interno

Por padrão, o Compose cria uma rede bridge onde todos os serviços se comunicam pelo nome do serviço. Exemplo: o serviço app acessa o banco via db:5432.

4.2. Redes customizadas para isolamento

services:
  frontend:
    networks:
      - front-net
  backend:
    networks:
      - back-net
      - front-net
  db:
    networks:
      - back-net

networks:
  front-net:
  back-net:

Isso permite que db seja inacessível ao frontend, aumentando a segurança.

4.3. Expondo portas vs comunicação interna

  • Use ports apenas para serviços que precisam ser acessados do host (ex: 80:80).
  • Para comunicação interna, confie no DNS interno do Compose (não exponha portas desnecessárias).

5. Volumes e persistência de dados com Compose

5.1. Volumes nomeados vs bind mounts

volumes:
  dbdata:      # volume nomeado (gerenciado pelo Docker)

services:
  db:
    volumes:
      - dbdata:/var/lib/postgresql/data   # persistência segura
  app:
    volumes:
      - ./src:/app/src                    # bind mount para desenvolvimento
  • Volumes nomeados: ideais para dados de produção (banco, logs).
  • Bind mounts: úteis para desenvolvimento (código sincronizado).

5.2. Compartilhamento entre serviços

volumes:
  shared-cache:

services:
  app:
    volumes:
      - shared-cache:/cache
  worker:
    volumes:
      - shared-cache:/cache

5.3. Estratégias para dados críticos

  • Banco de dados: sempre usar volume nomeado.
  • Logs: bind mount para diretório de logs no host ou volume nomeado.
  • Uploads: volume nomeado para evitar perda ao recriar containers.

6. Variáveis de ambiente e configuração por ambiente

6.1. Arquivo .env e substituição

Crie um arquivo .env na raiz:

DB_PASSWORD=minhasenha
APP_PORT=3000

No docker-compose.yml:

services:
  app:
    environment:
      - DB_PASSWORD=${DB_PASSWORD}
      - APP_PORT=${APP_PORT}

6.2. Perfis (profiles) para diferentes cenários

services:
  db:
    image: postgres:15
  app-dev:
    profiles: ["dev"]
    build: ./app
  app-prod:
    profiles: ["prod"]
    image: myapp:latest

Execute com: docker-compose --profile dev up

6.3. Segredos e boas práticas

  • Nunca hardcode senhas no YAML.
  • Use arquivos .env ignorados pelo Git (.gitignore).
  • Para produção, considere Docker Secrets ou ferramentas como Vault.

7. Exemplo prático: aplicação web com banco e cache

7.1. Serviços definidos

version: '3.8'

services:
  app:
    build: ./app
    ports:
      - "3000:3000"
    environment:
      - DB_HOST=db
      - DB_PORT=5432
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - ./app:/usr/src/app
    networks:
      - app-net

  db:
    image: postgres:15-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-net

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - app-net

volumes:
  pgdata:
  redisdata:

networks:
  app-net:
    driver: bridge

7.2. Comandos essenciais

# Iniciar todos os serviços
docker-compose up -d

# Ver logs em tempo real
docker-compose logs -f

# Executar comando em um serviço
docker-compose exec app npm test

# Listar serviços em execução
docker-compose ps

# Parar e remover tudo
docker-compose down -v

8. Do Compose ao Kubernetes: migração e similaridades

8.1. Mapeamento de conceitos

Docker Compose Kubernetes
Service Deployment + Service
Network NetworkPolicy
Volume PersistentVolumeClaim
depends_on Init Containers
ports Service NodePort/LoadBalancer

8.2. Ferramentas de conversão

O kompose converte automaticamente:

kompose convert -f docker-compose.yml

Isso gera arquivos YAML do Kubernetes prontos para uso com kubectl apply.

8.3. Limitações do Compose para produção

  • Escalabilidade: não suporta auto-scaling horizontal.
  • Alta disponibilidade: opera em um único host.
  • Rolling updates: não gerencia atualizações graduais.
  • Monitoramento: sem integração nativa com métricas e alertas.

Migre para Kubernetes quando precisar de resiliência multi-host, escalabilidade automática e gerenciamento avançado de tráfego.


Referências