Docker & Kubernetes

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

18 min de leitura

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

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

<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 &quot;rodando&quot;, não necessariamente &quot;pronto para receber conexões&quot;. É 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: &#039;3.9&#039;

services:

postgres:

image: postgres:15-alpine

environment:

POSTGRES_USER: usuario

POSTGRES_PASSWORD: senha123

POSTGRES_DB: minha_app

ports:

  • &quot;5432:5432&quot;

volumes:

  • postgres_data:/var/lib/postgresql/data

api:

image: my-api:latest

build: .

environment:

DATABASE_URL: postgresql://usuario:senha123@postgres:5432/minha_app

ports:

  • &quot;3000:3000&quot;

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: &#039;3.9&#039;

services:

postgres:

image: postgres:15-alpine

environment:

POSTGRES_USER: usuario

POSTGRES_PASSWORD: senha123

POSTGRES_DB: minha_app

healthcheck:

test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U usuario&quot;]

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:

  • &quot;3000:3000&quot;

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: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U usuario&quot;]

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: [&quot;CMD&quot;, &quot;redis-cli&quot;, &quot;ping&quot;]

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:

  • &quot;3000:3000&quot;

healthcheck:

test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:3000/health&quot;]

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: [&quot;CMD&quot;, &quot;mysqladmin&quot;, &quot;ping&quot;, &quot;-h&quot;, &quot;localhost&quot;]

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 &quot;perfil&quot; 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: &#039;3.9&#039;

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: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U usuario&quot;]

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:

  • &quot;3000:3000&quot;

depends_on:

postgres:

condition: service_healthy

healthcheck:

test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:3000/health&quot;]

interval: 10s

timeout: 5s

retries: 3

start_period: 30s

Cache: opcional, perfil &#039;cache&#039;

redis:

image: redis:7-alpine

profiles: [&quot;cache&quot;, &quot;full&quot;]

ports:

  • &quot;6379:6379&quot;

healthcheck:

test: [&quot;CMD&quot;, &quot;redis-cli&quot;, &quot;ping&quot;]

interval: 5s

timeout: 3s

retries: 5

Message Queue: opcional, perfil &#039;queue&#039;

rabbitmq:

image: rabbitmq:3.12-management-alpine

profiles: [&quot;queue&quot;, &quot;full&quot;]

environment:

RABBITMQ_DEFAULT_USER: user

RABBITMQ_DEFAULT_PASS: password

ports:

  • &quot;5672:5672&quot;
  • &quot;15672:15672&quot;

healthcheck:

test: [&quot;CMD&quot;, &quot;rabbitmq-diagnostics&quot;, &quot;ping&quot;]

interval: 10s

timeout: 5s

retries: 5

Logging centralizado: apenas em desenvolvimento completo

kibana:

image: docker.elastic.co/kibana/kibana:8.10.0

profiles: [&quot;dev&quot;, &quot;full&quot;]

environment:

ELASTICSEARCH_HOSTS: http://elasticsearch:9200

ports:

  • &quot;5601:5601&quot;

depends_on:

elasticsearch:

condition: service_healthy

elasticsearch:

image: docker.elastic.co/elasticsearch/elasticsearch:8.10.0

profiles: [&quot;dev&quot;, &quot;full&quot;]

environment:

discovery.type: single-node

xpack.security.enabled: &quot;false&quot;

ports:

  • &quot;9200:9200&quot;

healthcheck:

test: [&quot;CMD-SHELL&quot;, &quot;curl -f http://localhost:9200/_cluster/health || exit 1&quot;]

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: &#039;3.9&#039;

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: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U usuario&quot;]

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:

  • &quot;3000:3000&quot;

secrets:

  • postgres_password
  • jwt_secret

depends_on:

postgres:

condition: service_healthy

healthcheck:

test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:3000/health&quot;]

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: &#039;3.9&#039;

services:

postgres:

image: postgres:15-alpine

environment:

POSTGRES_USER: usuario

POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

POSTGRES_DB: minha_app

healthcheck:

test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U usuario&quot;]

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:

  • &quot;3000:3000&quot;

depends_on:

postgres:

condition: service_healthy

healthcheck:

test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:3000/health&quot;]

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: &#039;3.9&#039;

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 &quot;chave_secreta_real&quot; | 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: &#039;3.9&#039;

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: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U app_user&quot;]

interval: 10s

timeout: 5s

retries: 5

start_period: 10s

networks:

  • app_network

ports:

  • &quot;5432:5432&quot;

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} &amp;&amp; redis://redis:6379 || &quot;&quot;

ports:

  • &quot;3000:3000&quot;

depends_on:

postgres:

condition: service_healthy

redis:

condition: service_healthy

healthcheck:

test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:3000/health&quot;]

interval: 15s

timeout: 5s

retries: 3

start_period: 30s

networks:

  • app_network

restart: unless-stopped

=== SERVIÇOS OPCIONAIS ===

redis:

image: redis:7-alpine

profiles: [&quot;cache&quot;, &quot;full&quot;]

container_name: app_redis

command: redis-server --appendonly yes

volumes:

  • redis_data:/data

healthcheck:

test: [&quot;CMD&quot;, &quot;redis-cli&quot;, &quot;ping&quot;]

interval: 10s

timeout: 3s

retries: 5

networks:

  • app_network

ports:

  • &quot;6379:6379&quot;

rabbitmq:

image: rabbitmq:3.12-management-alpine

profiles: [&quot;queue&quot;, &quot;full&quot;]

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: [&quot;CMD&quot;, &quot;rabbitmq-diagnostics&quot;, &quot;ping&quot;]

interval: 10s

timeout: 5s

retries: 5

networks:

  • app_network

ports:

  • &quot;5672:5672&quot;
  • &quot;15672:15672&quot;

pgadmin:

image: dpage/pgadmin4:latest

profiles: [&quot;dev&quot;, &quot;full&quot;]

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:

  • &quot;5050:80&quot;

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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Docker & Kubernetes

Guia Completo de Linkerd como Alternativa Leve ao Istio na Prática
Guia Completo de Linkerd como Alternativa Leve ao Istio na Prática

O que é Linkerd e por que considerar uma alternativa ao Istio Linkerd é um se...

Dominando Imagens Distroless e Alpine: Segurança e Tamanho em Produção em Projetos Reais
Dominando Imagens Distroless e Alpine: Segurança e Tamanho em Produção em Projetos Reais

O Que São Imagens Distroless? Imagens distroless são contêineres Docker minim...

Dominando Custom Resource Definitions: Estendendo a API do Kubernetes em Projetos Reais
Dominando Custom Resource Definitions: Estendendo a API do Kubernetes em Projetos Reais

O que são Custom Resource Definitions (CRDs)? Custom Resource Definitions são...