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údeinterval: 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.