<h2>Docker Compose Avançado: Profiles, Depends On, Healthchecks e Secrets</h2>
<p>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?</p>
<p>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.</p>
<h2>Depends On: Orquestrando a Sequência de Inicialização</h2>
<h3>O Problema Real</h3>
<p>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 <code>depends_on</code> foi criado justamente para isso.</p>
<h3>Como Funciona</h3>
<p>A diretiva <code>depends_on</code> 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 <code>depends_on</code> em conjunto com healthchecks, que veremos adiante.</p>
<h3>Exemplo Prático</h3>
<pre><code class="language-yaml">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:</code></pre>
<p>Neste exemplo, a <code>api</code> aguardará o <code>postgres</code> 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.</p>
<h3>Condition: Service Healthy</h3>
<p>A versão mais robusta usa <code>condition: service_healthy</code>, que aguarda o healthcheck passar:</p>
<pre><code class="language-yaml">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:</code></pre>
<p>Agora sim, a API só iniciará quando o PostgreSQL responder positivamente ao healthcheck. Essa é a forma correta de fazer orquestração robusta.</p>
<h2>Healthchecks: Monitorando a Saúde dos Serviços</h2>
<h3>Por Que Healthchecks Importam</h3>
<p>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.</p>
<h3>Anatomia de um Healthcheck</h3>
<p>Todo healthcheck tem quatro parâmetros essenciais:</p>
<ul>
<li><code>test</code>: O comando que verifica a saúde</li>
<li><code>interval</code>: Frequência de execução (padrão: 30s)</li>
<li><code>timeout</code>: Tempo máximo para o teste responder (padrão: 30s)</li>
<li><code>retries</code>: Quantas falhas consecutivas antes de marcar como unhealthy (padrão: 3)</li>
</ul>
<h3>Exemplos Práticos por Tipo de Serviço</h3>
<p><strong>PostgreSQL:</strong></p>
<pre><code class="language-yaml">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</code></pre>
<p><strong>Redis:</strong></p>
<pre><code class="language-yaml">redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5</code></pre>
<p><strong>API Node.js/Express:</strong></p>
<pre><code class="language-yaml">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</code></pre>
<p>Note o <code>start_period</code>: dá um tempo de graça antes de começar a verificar. Aplicações podem levar alguns segundos para inicializar completamente.</p>
<p><strong>MySQL:</strong></p>
<pre><code class="language-yaml">mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: root123
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5</code></pre>
<h2>Profiles: Gerenciando Diferentes Ambientes e Cargas de Trabalho</h2>
<h3>O Desafio dos Múltiplos Ambientes</h3>
<p>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 <code>docker-compose.yml</code> é caótico.</p>
<p>Profiles resolvem isso permitindo que você defina qual serviço faz parte de qual "perfil" de execução.</p>
<h3>Como Funciona</h3>
<p>Cada serviço pode ter um atributo <code>profiles</code> (lista). Quando você inicia o Compose com <code>--profile</code>, apenas serviços com aquele profile (ou sem nenhum profile) iniciam.</p>
<h3>Exemplo Completo: Arquitetura Realista</h3>
<pre><code class="language-yaml">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:</code></pre>
<h3>Usando os Profiles</h3>
<pre><code class="language-bash"># 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</code></pre>
<p>Um detalhe importante: serviços <strong>sem profile definido</strong> sempre são iniciados, independente de qual profile você especificar.</p>
<h2>Secrets: Protegendo Informações Sensíveis</h2>
<h3>O Perigo das Variáveis de Ambiente Claras</h3>
<p>Colocar senhas, tokens e chaves diretamente em variáveis de ambiente no <code>docker-compose.yml</code> é 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.</p>
<h3>Como Funciona</h3>
<p>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.</p>
<h3>Exemplo com Arquivo Externo (Recomendado)</h3>
<p>Primeiro, crie um arquivo <code>.env.example</code> para documentar quais secrets são necessários:</p>
<pre><code># .env.example
DB_PASSWORD=seu_password_aqui
POSTGRES_PASSWORD=seu_password_aqui
API_JWT_SECRET=seu_secret_aqui</code></pre>
<p>Crie o arquivo real <code>.env</code> (que deve estar no <code>.gitignore</code>):</p>
<pre><code># .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</code></pre>
<p>Agora o <code>docker-compose.yml</code>:</p>
<pre><code class="language-yaml">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:</code></pre>
<p>Aguarde, isso está meio confuso porque o Docker Compose não interpola variáveis do <code>.env</code> em secrets diretamente. A forma correta é usar um script ou ferramenta. Vou mostrar a abordagem mais prática:</p>
<pre><code class="language-yaml">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:</code></pre>
<p>Arquivo <code>.env</code>:</p>
<pre><code>POSTGRES_PASSWORD=senha_super_secreta_123
API_JWT_SECRET=chave_jwt_aleatoria_gerada_com_seguranca</code></pre>
<p>E certifique-se de adicionar ao <code>.gitignore</code>:</p>
<pre><code>.env
.env.local</code></pre>
<h3>Secrets Verdadeiros em Produção</h3>
<p>Em Swarm Mode ou Kubernetes, você usaria secrets reais:</p>
<pre><code class="language-yaml">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</code></pre>
<p>Criaria assim no Docker Swarm:</p>
<pre><code class="language-bash">echo "chave_secreta_real" | docker secret create api_jwt -
docker stack deploy -c docker-compose.yml minha_stack</code></pre>
<h2>Exemplo Integrado Completo</h2>
<p>Para consolidar tudo que aprendemos, aqui está um <code>docker-compose.yml</code> profissional que usa <code>depends_on</code>, <code>healthchecks</code>, <code>profiles</code> e <code>secrets</code>:</p>
<pre><code class="language-yaml">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:</code></pre>
<p>Arquivo <code>.env</code>:</p>
<pre><code>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</code></pre>
<p>Comandos para usar:</p>
<pre><code class="language-bash"># 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</code></pre>
<h2>Conclusão</h2>
<p>Dominando Docker Compose avançado não é sobre memorizar sintaxe — é sobre entender os problemas reais que cada recurso resolve. O <code>depends_on</code> com <code>service_healthy</code> 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 <code>profiles</code> permitem que um único arquivo <code>docker-compose.yml</code> seja reutilizável em múltiplos ambientes sem duplicação caótica de código. E os <code>secrets</code>, mesmo em sua forma simples com arquivos <code>.env</code>, protegem suas credenciais contra exposição acidental.</p>
<p>Dois aprendizados principais você leva: primeiro, sempre combine <code>depends_on</code> com <code>condition: service_healthy</code> 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 <code>.env</code> — documente com <code>.env.example</code> e deixe cada desenvolvedor configurar sua própria cópia.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://docs.docker.com/compose/profiles/" target="_blank" rel="noopener noreferrer">Docker Compose Official Documentation - Profiles</a></li>
<li><a href="https://docs.docker.com/compose/compose-file/05-services/#depends_on" target="_blank" rel="noopener noreferrer">Docker Compose Official Documentation - Depends On</a></li>
<li><a href="https://docs.docker.com/engine/reference/builder/#healthcheck" target="_blank" rel="noopener noreferrer">Docker Compose Official Documentation - Healthchecks</a></li>
<li><a href="https://docs.docker.com/engine/swarm/secrets/" target="_blank" rel="noopener noreferrer">Docker Secrets Best Practices</a></li>
<li><a href="https://docs.docker.com/compose/compose-file/03-compose-file/" target="_blank" rel="noopener noreferrer">Compose File Reference - Version 3.9</a></li>
</ul>
<p><!-- FIM --></p>