Dominando Docker Compose Avançado: Profiles, Depends On, Healthchecks e Secrets em Projetos Reais Já leu

Docker Compose Avançado: Profiles, Depends On, Healthchecks e Secrets Quando você começa a trabalhar com Docker Compose em ambientes de produção ou em projetos mais complexos, rapidamente percebe que a configuração básica não é suficiente. Você enfrenta problemas reais: como iniciar serviços na ordem correta? Como gerenciar diferentes ambientes sem duplicar código? Como garantir que um container está realmente saudável antes de enviar requisições? E como lidar com informações sensíveis sem expô-las no código? Este artigo aborda exatamente esses problemas usando quatro recursos poderosos do Docker Compose que separam os iniciantes dos profissionais. Vamos evitar teoria vaga e focar em aplicação prática imediata. Depends On: Orquestrando a Sequência de Inicialização O Problema Real Imagine você ter um serviço de API que precisa conectar a um banco de dados PostgreSQL. Se o Docker Compose iniciar a API antes do banco estar completamente pronto, a aplicação falhará na tentativa de conectar. O foi criado justamente para isso. Como Funciona A diretiva garante

Docker Compose Avançado: Profiles, Depends On, Healthchecks e Secrets

Quando você começa a trabalhar com Docker Compose em ambientes de produção ou em projetos mais complexos, rapidamente percebe que a configuração básica não é suficiente. Você enfrenta problemas reais: como iniciar serviços na ordem correta? Como gerenciar diferentes ambientes sem duplicar código? Como garantir que um container está realmente saudável antes de enviar requisições? E como lidar com informações sensíveis sem expô-las no código?

Este artigo aborda exatamente esses problemas usando quatro recursos poderosos do Docker Compose que separam os iniciantes dos profissionais. Vamos evitar teoria vaga e focar em aplicação prática imediata.

Depends On: Orquestrando a Sequência de Inicialização

O Problema Real

Imagine você ter um serviço de API que precisa conectar a um banco de dados PostgreSQL. Se o Docker Compose iniciar a API antes do banco estar completamente pronto, a aplicação falhará na tentativa de conectar. O depends_on foi criado justamente para isso.

Como Funciona

A diretiva depends_on garante que um serviço aguarde o início de outro antes de ser inicializado. Porém — e isso é crítico — ela apenas aguarda o container estar "rodando", não necessariamente "pronto para receber conexões". É por isso que usamos depends_on em conjunto com healthchecks, que veremos adiante.

Exemplo Prático

version: '3.9'

services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: usuario
      POSTGRES_PASSWORD: senha123
      POSTGRES_DB: minha_app
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  api:
    image: my-api:latest
    build: .
    environment:
      DATABASE_URL: postgresql://usuario:senha123@postgres:5432/minha_app
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_started

volumes:
  postgres_data:

Neste exemplo, a api aguardará o postgres estar em execução antes de iniciar. Mas repare bem: isso não significa que o PostgreSQL já aceitará conexões. Para isso, precisamos de healthchecks.

Condition: Service Healthy

A versão mais robusta usa condition: service_healthy, que aguarda o healthcheck passar:

version: '3.9'

services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: usuario
      POSTGRES_PASSWORD: senha123
      POSTGRES_DB: minha_app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U usuario"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - postgres_data:/var/lib/postgresql/data

  api:
    image: my-api:latest
    build: .
    environment:
      DATABASE_URL: postgresql://usuario:senha123@postgres:5432/minha_app
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  postgres_data:

Agora sim, a API só iniciará quando o PostgreSQL responder positivamente ao healthcheck. Essa é a forma correta de fazer orquestração robusta.

Healthchecks: Monitorando a Saúde dos Serviços

Por Que Healthchecks Importam

Um container rodando não significa que o serviço está operacional. O PostgreSQL pode estar iniciando ainda, o Redis pode estar carregando dados em memória, sua API pode estar travada em uma conexão lenta. Healthchecks permitem que o Docker Compose (e o Docker em geral) saiba realmente se um serviço está pronto.

Anatomia de um Healthcheck

Todo healthcheck tem quatro parâmetros essenciais:

  • test: O comando que verifica a saúde
  • interval: Frequência de execução (padrão: 30s)
  • timeout: Tempo máximo para o teste responder (padrão: 30s)
  • retries: Quantas falhas consecutivas antes de marcar como unhealthy (padrão: 3)

Exemplos Práticos por Tipo de Serviço

PostgreSQL:

postgres:
  image: postgres:15-alpine
  environment:
    POSTGRES_USER: usuario
    POSTGRES_PASSWORD: senha123
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U usuario"]
    interval: 10s
    timeout: 5s
    retries: 5

Redis:

redis:
  image: redis:7-alpine
  healthcheck:
    test: ["CMD", "redis-cli", "ping"]
    interval: 5s
    timeout: 3s
    retries: 5

API Node.js/Express:

api:
  image: my-api:latest
  build: .
  ports:
    - "3000:3000"
  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
    interval: 10s
    timeout: 5s
    retries: 3
    start_period: 30s

Note o start_period: dá um tempo de graça antes de começar a verificar. Aplicações podem levar alguns segundos para inicializar completamente.

MySQL:

mysql:
  image: mysql:8
  environment:
    MYSQL_ROOT_PASSWORD: root123
  healthcheck:
    test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
    interval: 10s
    timeout: 5s
    retries: 5

Profiles: Gerenciando Diferentes Ambientes e Cargas de Trabalho

O Desafio dos Múltiplos Ambientes

Você tem uma arquitetura complexa: banco de dados principal, cache, queue, serviço de logs, ferramentas de debug. Em produção, você quer tudo. Em desenvolvimento local, talvez queira apenas o essencial. Em testes de performance, você quer ligar apenas certos componentes. Duplicar o docker-compose.yml é caótico.

Profiles resolvem isso permitindo que você defina qual serviço faz parte de qual "perfil" de execução.

Como Funciona

Cada serviço pode ter um atributo profiles (lista). Quando você inicia o Compose com --profile, apenas serviços com aquele profile (ou sem nenhum profile) iniciam.

Exemplo Completo: Arquitetura Realista

version: '3.9'

services:
  # Serviços sempre ativos (sem profile)
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: usuario
      POSTGRES_PASSWORD: senha123
      POSTGRES_DB: minha_app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U usuario"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - postgres_data:/var/lib/postgresql/data

  api:
    image: my-api:latest
    build: .
    environment:
      DATABASE_URL: postgresql://usuario:senha123@postgres:5432/minha_app
      REDIS_URL: redis://redis:6379
      QUEUE_URL: amqp://rabbitmq:5672
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s

  # Cache: opcional, perfil 'cache'
  redis:
    image: redis:7-alpine
    profiles: ["cache", "full"]
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  # Message Queue: opcional, perfil 'queue'
  rabbitmq:
    image: rabbitmq:3.12-management-alpine
    profiles: ["queue", "full"]
    environment:
      RABBITMQ_DEFAULT_USER: user
      RABBITMQ_DEFAULT_PASS: password
    ports:
      - "5672:5672"
      - "15672:15672"
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Logging centralizado: apenas em desenvolvimento completo
  kibana:
    image: docker.elastic.co/kibana/kibana:8.10.0
    profiles: ["dev", "full"]
    environment:
      ELASTICSEARCH_HOSTS: http://elasticsearch:9200
    ports:
      - "5601:5601"
    depends_on:
      elasticsearch:
        condition: service_healthy

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.10.0
    profiles: ["dev", "full"]
    environment:
      discovery.type: single-node
      xpack.security.enabled: "false"
    ports:
      - "9200:9200"
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data

volumes:
  postgres_data:
  elasticsearch_data:

Usando os Profiles

# Apenas o essencial (API + PostgreSQL)
docker-compose up

# Com cache
docker-compose --profile cache up

# Stack completa de desenvolvimento
docker-compose --profile dev --profile full up

# Tudo, inclusive monitoring
docker-compose --profile full up

Um detalhe importante: serviços sem profile definido sempre são iniciados, independente de qual profile você especificar.

Secrets: Protegendo Informações Sensíveis

O Perigo das Variáveis de Ambiente Claras

Colocar senhas, tokens e chaves diretamente em variáveis de ambiente no docker-compose.yml é um erro grave. Essas credenciais ficam visíveis no seu repositório Git, nos logs do container, em qualquer inspeção do processo. Secrets foi criado para resolver isso.

Como Funciona

Docker Compose permite definir secrets de duas formas: inline (apenas para desenvolvimento local) ou via arquivo externo (para produção). Os secrets são montados como arquivos dentro do container, não como variáveis de ambiente, tornando-os menos acessíveis.

Exemplo com Arquivo Externo (Recomendado)

Primeiro, crie um arquivo .env.example para documentar quais secrets são necessários:

# .env.example
DB_PASSWORD=seu_password_aqui
POSTGRES_PASSWORD=seu_password_aqui
API_JWT_SECRET=seu_secret_aqui

Crie o arquivo real .env (que deve estar no .gitignore):

# .env (NÃO commitar isso!)
DB_PASSWORD=senha_super_secreta_123
POSTGRES_PASSWORD=senha_super_secreta_123
API_JWT_SECRET=chave_jwt_aleatoria_gerada_com_seguranca

Agora o docker-compose.yml:

version: '3.9'

services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: usuario
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
      POSTGRES_DB: minha_app
    secrets:
      - postgres_password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U usuario"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - postgres_data:/var/lib/postgresql/data

  api:
    image: my-api:latest
    build: .
    environment:
      DATABASE_URL: postgresql://usuario@postgres:5432/minha_app
      JWT_SECRET_FILE: /run/secrets/jwt_secret
    ports:
      - "3000:3000"
    secrets:
      - postgres_password
      - jwt_secret
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s

secrets:
  postgres_password:
    file: .env
    # Na verdade, para arquivo externo único, você precisa fazer assim:
  jwt_secret:
    file: .env

volumes:
  postgres_data:

Aguarde, isso está meio confuso porque o Docker Compose não interpola variáveis do .env em secrets diretamente. A forma correta é usar um script ou ferramenta. Vou mostrar a abordagem mais prática:

version: '3.9'

services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: usuario
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: minha_app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U usuario"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - postgres_data:/var/lib/postgresql/data

  api:
    image: my-api:latest
    build: .
    environment:
      DATABASE_URL: postgresql://usuario:${POSTGRES_PASSWORD}@postgres:5432/minha_app
      JWT_SECRET: ${API_JWT_SECRET}
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s

volumes:
  postgres_data:

Arquivo .env:

POSTGRES_PASSWORD=senha_super_secreta_123
API_JWT_SECRET=chave_jwt_aleatoria_gerada_com_seguranca

E certifique-se de adicionar ao .gitignore:

.env
.env.local

Secrets Verdadeiros em Produção

Em Swarm Mode ou Kubernetes, você usaria secrets reais:

version: '3.9'

services:
  api:
    image: my-api:latest
    environment:
      JWT_SECRET_FILE: /run/secrets/api_jwt
    secrets:
      - api_jwt

secrets:
  api_jwt:
    external: true  # Criado separadamente no Swarm/Kubernetes

Criaria assim no Docker Swarm:

echo "chave_secreta_real" | docker secret create api_jwt -
docker stack deploy -c docker-compose.yml minha_stack

Exemplo Integrado Completo

Para consolidar tudo que aprendemos, aqui está um docker-compose.yml profissional que usa depends_on, healthchecks, profiles e secrets:

version: '3.9'

services:
  # === SERVIÇOS CORE (sempre ativos) ===

  postgres:
    image: postgres:15-alpine
    container_name: app_postgres
    environment:
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: app_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app_user"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s
    networks:
      - app_network
    ports:
      - "5432:5432"

  api:
    image: my-api:latest
    build:
      context: .
      dockerfile: Dockerfile
    container_name: app_api
    environment:
      NODE_ENV: production
      DATABASE_URL: postgresql://app_user:${DB_PASSWORD}@postgres:5432/app_db
      JWT_SECRET: ${JWT_SECRET}
      REDIS_URL: ${REDIS_ENABLED:-false} && redis://redis:6379 || ""
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 30s
    networks:
      - app_network
    restart: unless-stopped

  # === SERVIÇOS OPCIONAIS ===

  redis:
    image: redis:7-alpine
    profiles: ["cache", "full"]
    container_name: app_redis
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
    networks:
      - app_network
    ports:
      - "6379:6379"

  rabbitmq:
    image: rabbitmq:3.12-management-alpine
    profiles: ["queue", "full"]
    container_name: app_rabbitmq
    environment:
      RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-guest}
      RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS:-guest}
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app_network
    ports:
      - "5672:5672"
      - "15672:15672"

  pgadmin:
    image: dpage/pgadmin4:latest
    profiles: ["dev", "full"]
    container_name: app_pgadmin
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@example.com
      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin}
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - app_network
    ports:
      - "5050:80"

networks:
  app_network:
    driver: bridge

volumes:
  postgres_data:
  redis_data:
  rabbitmq_data:

Arquivo .env:

DB_PASSWORD=senha_super_secreta_123
JWT_SECRET=jwt_key_aleatorio_com_entropia_suficiente
RABBITMQ_USER=rabbitmq_user
RABBITMQ_PASS=rabbitmq_pass
PGADMIN_PASSWORD=pgadmin_password

Comandos para usar:

# Desenvolvimento básico (API + PostgreSQL)
docker-compose up

# Com cache Redis
docker-compose --profile cache up

# Stack completa com todas as ferramentas
docker-compose --profile full up

# Adicione o pgadmin ao full stack
docker-compose --profile full --profile dev up

# Ver logs
docker-compose logs -f api

# Ver status de health
docker-compose ps

Conclusão

Dominando Docker Compose avançado não é sobre memorizar sintaxe — é sobre entender os problemas reais que cada recurso resolve. O depends_on com service_healthy garante que sua aplicação inicia apenas quando suas dependências estão genuinamente prontas, eliminando race conditions que aparecem aleatoriamente em produção. Os profiles permitem que um único arquivo docker-compose.yml seja reutilizável em múltiplos ambientes sem duplicação caótica de código. E os secrets, mesmo em sua forma simples com arquivos .env, protegem suas credenciais contra exposição acidental.

Dois aprendizados principais você leva: primeiro, sempre combine depends_on com condition: service_healthy e healthchecks bem configurados — iniciar um container não significa que ele está pronto; segundo, use profiles generosamente em projetos reais; seu colega da manhã seguinte agradeça quando não precisar editar o Compose para remover containers desnecessários. E terceiro (sim, três aprendizados): nunca commite credenciais em seu repositório, nem mesmo em .env — documente com .env.example e deixe cada desenvolvedor configurar sua própria cópia.

Referências


Artigos relacionados