Alternar tema

Dependências no Docker Compose: healthcheck para resolver a ordem de inicialização do banco

Easton editorial illustration: environment switchboard

Sexta-feira, dez da noite, e as mesmas mensagens de erro já tinham rolado no terminal mais de dez vezes.

O contêiner da aplicação reiniciava sem parar. O banco até iniciava, mas sempre demorava um pouco mais. Conferi o docker-compose.yml: depends_on estava configurado. Então por que não funcionava?

Esse problema já atrapalhou muita gente. No ambiente local, as duas primeiras tentativas com docker-compose up falham, e só depois de alguns reinícios e uns dez segundos tudo começa a funcionar.

A causa é simples: o depends_on do Docker controla a ordem de inicialização dos contêineres, mas não verifica se o serviço está realmente pronto.

Neste artigo, você vai aprender:

  • as três condições de depends_on — 90% das pessoas conhecem apenas a padrão;
  • como configurar corretamente o healthcheck do PostgreSQL e do MySQL, com exemplos completos;
  • uma alternativa moderna aos scripts wait-for-it;
  • um checklist de diagnóstico para inicializar seus contêineres com estabilidade.

Por que depends_on não é suficiente: iniciar não significa estar pronto

A documentação oficial do Docker traz uma frase importante, mas fácil de ignorar:

Compose does not wait until a container is “ready”, only until it’s running.

Em outras palavras: o Compose espera o contêiner iniciar, mas não espera o serviço ficar realmente disponível.

A diferença entre iniciar o contêiner e disponibilizar o serviço

Imagine o processo de inicialização de um contêiner PostgreSQL:

  1. 0 segundo: o Docker inicia o contêiner e o processo postgresdepends_on já libera a aplicação aqui
  2. 2 segundos: inicialização do diretório de dados
  3. 5 segundos: carregamento dos arquivos de configuração
  4. 8 segundos: execução dos scripts de init, se houver
  5. 12 segundos: finalmente fica pronto para aceitar conexões

Há uma diferença de 12 segundos. Se a aplicação web tentar conectar no primeiro segundo, o resultado inevitável será Connection refused.

Já encontrei casos ainda mais extremos. Em um projeto legado que mantive, o script de inicialização do banco precisava importar 500 MB de dados de teste, um processo que levava 40 segundos. Com o depends_on padrão, o contêiner da aplicação precisava falhar e reiniciar pelo menos cinco vezes antes de conseguir conectar.

As três condições de depends_on

Muita gente não sabe que depends_on aceita três condições:

services:
  web:
    depends_on:
      db:
        condition: service_started  # Padrão: basta iniciar o contêiner
        # condition: service_healthy  # Aguarda o healthcheck passar
        # condition: service_completed_successfully  # Aguarda o contêiner encerrar com sucesso (adequado para init)

service_started (padrão): continua assim que o contêiner entra no estado running. É por isso que o problema persiste mesmo com depends_on configurado.

service_healthy: aguarda o healthcheck passar e o contêiner entrar no estado healthy. É o comportamento de que realmente precisamos.

service_completed_successfully: aguarda o contêiner encerrar com sucesso, com código de saída 0. É adequada para tarefas executadas uma única vez, como migrações de dados.

Por que service_healthy não é o padrão?

Talvez você esteja se perguntando: se service_healthy é tão útil, por que não é a opção padrão?

Há dois motivos:

  1. nem todo serviço precisa de um healthcheck, como um worker totalmente stateless;
  2. você precisa definir o healthcheck, pois o Docker não sabe o que significa “estar pronto” para seu serviço.

Isso nos leva à próxima questão: como configurar o healthcheck.

Guia completo de configuração do healthcheck

O princípio do healthcheck é direto: o Docker executa um comando periodicamente. Se ele retornar 0, o serviço é considerado saudável; se retornar 1, é considerado não saudável.

Configuração completa de healthcheck

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]  # Comando de verificação
      interval: 10s       # Verifica a cada 10 segundos
      timeout: 5s         # Cada verificação expira após 5 segundos
      retries: 3          # Marca como unhealthy após 3 falhas
      start_period: 30s   # Falhas nos primeiros 30 segundos não contam em retries

Esses cinco parâmetros são importantes. Vamos analisar cada um.

test: comando de verificação

Há dois formatos:

# Opção 1: usar o shell (recomendada)
test: ["CMD-SHELL", "pg_isready -U postgres"]

# Opção 2: executar o comando diretamente, sem passar pelo shell
test: ["CMD", "pg_isready", "-U", "postgres"]

Na maioria dos casos, CMD-SHELL é suficiente, pois permite usar recursos do shell, como pipes e redirecionamento.

Armadilha comum: a ferramenta usada no comando precisa existir na imagem. Se você usar curl para testar um endpoint HTTP, mas a imagem não tiver curl, o healthcheck sempre falhará. Eu mesmo já perdi meia hora até perceber que precisava adicionar RUN apk add curl ao Dockerfile.

interval: intervalo entre verificações

Define a frequência das verificações. Um intervalo muito curto desperdiça recursos; um muito longo demora para detectar mudanças.

  • 10 segundos é um bom padrão para a maioria dos casos;
  • para serviços críticos, como bancos de dados, você pode usar 5 segundos;
  • para serviços leves, 15 a 30 segundos também funciona.

timeout: tempo limite de cada verificação

É o tempo máximo para uma verificação. Se o comando travar, o Docker aguarda esse período antes de desistir.

Um valor muito curto pode gerar falsos positivos; um valor muito longo atrasa a detecção de falhas. De 5 a 10 segundos é uma faixa segura.

retries: número de tentativas após falhas

Define quantas falhas consecutivas são necessárias para marcar o contêiner como unhealthy.

É um mecanismo contra oscilações. Uma instabilidade de rede ou uma breve recarga do banco pode causar uma falha isolada, e retries torna o sistema mais resistente.

De 3 a 5 tentativas costuma ser razoável. retries=1 é sensível demais; retries=10, lento demais.

start_period: período de tolerância na inicialização

Este é o parâmetro mais fácil de ignorar e uma fonte frequente de problemas.

As falhas durante start_period não contam em retries. Na prática, ele oferece um período de tolerância para o serviço iniciar.

Por que isso importa? Bancos de dados levam tempo para subir. O PostgreSQL precisa inicializar o diretório de dados; o MySQL precisa carregar os índices das tabelas. Sem start_period, o healthcheck pode começar a contabilizar falhas no segundo 2 e marcar o serviço como unhealthy antes mesmo de ele terminar a inicialização.

Valores recomendados:

  • PostgreSQL/MySQL: 30 a 60 segundos;
  • serviços leves, como Redis: 15 a 30 segundos;
  • serviços com muitos scripts de inicialização: até 120 segundos.

Costumo usar 60 segundos. Prefiro esperar um pouco mais a gerar um alerta incorreto.

Erros comuns e como evitá-los

Erro 1: referência incorreta a uma variável de ambiente

# ❌ Incorreto: o Compose interpola antes de iniciar, usando o valor da variável no host
test: ["CMD", "mysqladmin", "ping", "-p$MYSQL_ROOT_PASSWORD"]

# ✅ Correto: use $$ para que o shell do contêiner faça a expansão
test: ["CMD-SHELL", "mysqladmin ping -p$$MYSQL_ROOT_PASSWORD"]

Erro 2: start_period curto demais

# ❌ O banco ainda não terminou de iniciar, mas as falhas já são contabilizadas
healthcheck:
  test: ["CMD", "pg_isready"]
  interval: 5s
  retries: 3
  start_period: 10s  # Curto demais!

# ✅ Dê tempo suficiente para a inicialização
healthcheck:
  start_period: 60s  # Bem mais seguro

Erro 3: a ferramenta de verificação não existe

Esse erro é especialmente traiçoeiro, porque o Docker apenas registra a falha.

# ❌ Sem curl na imagem, o healthcheck sempre falhará
test: ["CMD", "curl", "-f", "http://localhost/health"]

# ✅ Garanta que a ferramenta exista ou use uma já presente na imagem
test: ["CMD", "wget", "--spider", "http://localhost/health"]  # Imagens Alpine incluem wget

Com o healthcheck configurado, vejamos como aplicá-lo a bancos de dados específicos.

Healthcheck do PostgreSQL na prática

A imagem oficial do PostgreSQL inclui uma ferramenta muito útil: pg_isready.

Ela foi criada especificamente para verificar se o PostgreSQL está pronto e é mais confiável do que escrever uma consulta SQL improvisada.

Configuração básica recomendada

version: '3.8'

services:
  web:
    image: node:20-alpine
    depends_on:
      db:
        condition: service_healthy  # Importante: aguarda o healthcheck passar
        restart: true  # Reinicia a aplicação quando o banco reiniciar
    environment:
      DATABASE_URL: postgresql://postgres:password@db:5432/myapp
    command: npm start

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 60s
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Entendendo o comando pg_isready

pg_isready -U postgres -d myapp
  • -U: define o usuário, que precisa existir;
  • -d: define o banco de dados, opcional, mas recomendado.

Por que incluir -U? Sem essa opção, pg_isready tenta conectar com o usuário atual do sistema e gera vários avisos no log. Isso não impede o funcionamento, mas polui a saída.

Configuração avançada com uma consulta real

pg_isready verifica se a porta está acessível, mas não garante que o banco execute consultas. Para uma verificação mais rigorosa:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres && psql -U postgres -d myapp -c 'SELECT 1'"]
  interval: 10s
  timeout: 10s  # Aumente o timeout por causa da consulta adicional
  retries: 3
  start_period: 60s

SELECT 1 é a consulta mais simples possível. Se ela for executada, o banco não apenas iniciou, como também consegue processar SQL normalmente.

Para a maioria dos casos, porém, o pg_isready básico é suficiente.

Resultado da execução

Depois de configurar, inicie os serviços:

$ docker-compose up

Creating network "myapp_default" ... done
Creating myapp_db_1 ... done
Waiting for myapp_db_1 to be healthy... Observe esta linha
Creating myapp_web_1 ... done

db_1   | PostgreSQL init process complete; ready for start up.
db_1   | database system is ready to accept connections
web_1  | Server listening on port 3000 A aplicação inicia quando o banco está pronto

Você verá uma pausa perceptível: o Docker aguarda o contêiner db ficar healthy. Isso pode levar de 30 a 60 segundos, mas em troca a aplicação inicia sem falhas.

Diagnóstico: o contêiner permanece unhealthy

Se o banco continuar no estado unhealthy, consulte o log do healthcheck:

# Ver o estado de saúde do contêiner
$ docker inspect --format='{{json .State.Health}}' myapp_db_1 | jq

{
  "Status": "unhealthy",
  "FailingStreak": 5,
  "Log": [
    {
      "Start": "2024-12-17T03:15:30Z",
      "End": "2024-12-17T03:15:30Z",
      "ExitCode": 1,
      "Output": "pg_isready: could not connect to server: Connection refused"
    }
  ]
}

As causas mais comuns são:

  1. start_period curto demais: as falhas começam a ser contabilizadas antes de o banco terminar a inicialização;
  2. usuário ou nome do banco incorreto: pg_isready não consegue conectar;
  3. falha na inicialização do PostgreSQL: consulte o log com docker logs myapp_db_1.

Healthcheck do MySQL na prática

Para o MySQL, usamos mysqladmin ping, a ferramenta administrativa incluída no próprio MySQL.

Configuração básica recomendada

version: '3.8'

services:
  web:
    image: node:20-alpine
    depends_on:
      db:
        condition: service_healthy
        restart: true
    environment:
      DATABASE_URL: mysql://root:password@db:3306/myapp
    command: npm start

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: myapp
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 60s
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:

Entendendo o comando mysqladmin ping

mysqladmin ping -h localhost -u root -ppassword
  • -h: endereço do host; dentro do contêiner, use localhost;
  • -u: nome do usuário;
  • -p: senha — observe que não há espaço entre -p e a senha.

Quando o MySQL está funcionando, o comando retorna:

mysqld is alive

O código de saída é 0, e o healthcheck passa.

Como tratar a senha corretamente

Opção 1: escrever a senha diretamente — adequada para desenvolvimento

healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"]

É simples, mas deixa a senha fixa na configuração.

Opção 2: usar uma variável de ambiente — recomendada

db:
  environment:
    MYSQL_ROOT_PASSWORD: password
  healthcheck:
    test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"]
    # Atenção: use $$, não $

Aqui existe uma armadilha: você precisa usar $$, e não $.

Por quê? O Docker Compose interpreta variáveis de ambiente antes de iniciar o contêiner. Com $MYSQL_ROOT_PASSWORD, ele procura a variável no host, não dentro do contêiner. $$ diz ao Compose para não expandi-la e deixar essa tarefa para o shell do contêiner.

Opção 3: verificação sem senha — a mais simples, mas controversa

healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]

Algumas configurações do MySQL permitem conexão local sem senha. Nesse caso, é a solução mais simples, mas não é recomendada em produção.

Cuidados específicos com o MySQL 8.0

O MySQL 8.0 usa por padrão o plugin de autenticação caching_sha2_password, que pode ser incompatível com clientes antigos. Se sua aplicação apresentar erro de autenticação, você pode forçar o método anterior:

db:
  image: mysql:8.0
  command: --default-authentication-plugin=mysql_native_password
  environment:
    MYSQL_ROOT_PASSWORD: password
    MYSQL_DATABASE: myapp
  healthcheck:
    test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"]
    interval: 10s
    timeout: 5s
    retries: 3
    start_period: 60s

Problemas comuns

Problema 1: Access denied for user 'root'@'localhost'

A senha está incorreta ou a variável de ambiente não foi aplicada. Verifique:

  1. se MYSQL_ROOT_PASSWORD está escrito corretamente;
  2. se a senha do healthcheck é a mesma;
  3. se você usou $$ para escapar a variável.

Problema 2: o contêiner demora para iniciar e permanece em starting

A inicialização do diretório de dados do MySQL leva tempo, principalmente na primeira execução. Defina um start_period suficiente; 60 segundos costuma bastar. Se um script de init importar muitos dados, podem ser necessários 120 segundos ou mais.

Problema 3: o healthcheck passa, mas a aplicação não conecta ao banco

Pode ser um problema de rede ou da configuração da aplicação. Verifique:

  1. se a string de conexão está correta — use db como host, não localhost;
  2. se a rede do Docker está configurada corretamente;
  3. com docker network inspect, se os contêineres estão na mesma rede.

Healthchecks para outros bancos de dados e serviços

Depois de entender PostgreSQL e MySQL, você pode aplicar o mesmo princípio a outros serviços. Veja uma referência rápida.

Redis

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

redis-cli ping retorna PONG com código de saída 0. Como o Redis inicia rapidamente, o start_period pode ser menor.

MongoDB

mongo:
  image: mongo:7
  healthcheck:
    test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
    interval: 10s
    timeout: 5s
    retries: 3
    start_period: 30s

A partir do MongoDB 6.0, mongosh substituiu o antigo comando mongo. Para versões anteriores:

test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"]

RabbitMQ

rabbitmq:
  image: rabbitmq:3-management-alpine
  healthcheck:
    test: ["CMD", "rabbitmq-diagnostics", "ping"]
    interval: 10s
    timeout: 5s
    retries: 3
    start_period: 40s

O RabbitMQ demora mais para iniciar, então recomenda-se um start_period de pelo menos 40 segundos.

Serviços HTTP em geral

Se o serviço oferece um endpoint de saúde, como /health ou /ping, use wget ou curl:

api:
  image: myapp:latest
  healthcheck:
    test: ["CMD", "wget", "--spider", "--quiet", "http://localhost:8080/health"]
    # Ou use curl, caso exista na imagem
    # test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
    interval: 10s
    timeout: 3s
    retries: 3
    start_period: 20s

--spider faz o wget apenas verificar o recurso, sem baixá-lo; --quiet suprime a saída.

Atenção: confirme se a imagem contém wget ou curl. Imagens Alpine normalmente incluem wget; imagens Debian/Ubuntu costumam incluir curl.

Serviços sem uma ferramenta específica

Se o serviço não oferece uma ferramenta de healthcheck, use netcat (nc) para testar a porta:

service:
  image: some-service:latest
  healthcheck:
    test: ["CMD-SHELL", "nc -z localhost 9000 || exit 1"]
    interval: 10s
    timeout: 3s
    retries: 3
    start_period: 30s

nc -z apenas verifica se a porta está aberta, sem estabelecer uma conexão completa. Esse método não confirma que o serviço está realmente pronto, portanto é menos preciso do que os anteriores.

O script wait-for-it ainda é necessário?

Ao pesquisar sobre ordem de inicialização no Docker, você provavelmente encontrará muitos artigos recomendando os scripts wait-for-it ou wait-for.

A ideia é acrescentar uma espera ao entrypoint do contêiner da aplicação, verificando se a porta TCP do serviço dependente está acessível.

Abordagem tradicional com wait-for-it

web:
  image: node:20-alpine
  depends_on:
    - db  # depends_on simples, sem verificar o estado de saúde
  volumes:
    - ./wait-for-it.sh:/wait-for-it.sh  # Monta o script
  command: ["/wait-for-it.sh", "db:5432", "--", "npm", "start"]

O wait-for-it.sh verifica repetidamente a porta db:5432 e só executa npm start quando consegue conectar.

Por que essa abordagem deixou de ser recomendada?

A boa prática em 2024 é: se puder usar o healthcheck nativo, não use um script.

Os motivos são:

  1. configuração mais clara: a lógica de saúde fica junto ao serviço de banco, deixando a dependência visível;
  2. nenhum arquivo adicional: você não precisa manter um script nem montar um volume;
  3. verificação mais completa: o healthcheck pode testar se o serviço realmente está pronto, enquanto uma porta TCP aberta é um sinal superficial;
  4. melhor reutilização: configure o healthcheck uma vez, e todos os serviços dependentes se beneficiam.

Um artigo bastante conhecido na comunidade resume a ideia no título: “Forget wait-for-it, use docker-compose healthcheck and depends_on instead”.

Quando wait-for-it ainda é necessário?

Há duas situações especiais.

Situação 1: não é possível modificar a imagem ou a configuração do Compose

Por exemplo, você usa uma imagem de terceiros sem healthcheck e não tem permissão para alterar a configuração. Nesse caso, adicionar wait-for-it no lado da aplicação pode ser a única opção.

Situação 2: é preciso aguardar vários serviços

./wait-for-it.sh db:5432 redis:6379 rabbitmq:5672 -- npm start

Embora depends_on também aceite vários serviços, essa sintaxe de wait-for-it pode ser mais concisa. Ainda assim, esse cenário não é tão comum.

Outras ferramentas alternativas

Além de wait-for-it, existem ferramentas parecidas:

  • dockerize: escrita em Go, oferece mais recursos e suporta templates com variáveis de ambiente;
  • wait-for: versão simplificada de wait-for-it, implementada somente em shell;
  • docker-compose-wait: escrita em Python, com suporte a verificações HTTP.

Mas, na prática, em 2024 vale a regra: se o healthcheck atende ao caso, não complique com essas ferramentas.

Checklist de diagnóstico e boas práticas

Configurou tudo e ainda não funciona? Siga este checklist; ele resolve 90% dos problemas.

Comandos para um diagnóstico rápido

# 1. Ver o estado de todos os contêineres
$ docker-compose ps

NAME       COMMAND    SERVICE   STATUS              PORTS
myapp_db   postgres   db        healthy             5432/tcp
myapp_web  npm start  web       running             0.0.0.0:3000->3000/tcp

# 2. Ver os detalhes do healthcheck
$ docker inspect --format='{{json .State.Health}}' myapp_db_1 | jq

# 3. Ver os logs dos contêineres
$ docker-compose logs db
$ docker-compose logs web

# 4. Acompanhar os logs em tempo real
$ docker-compose logs -f --tail=100

Árvore de decisão para problemas comuns

Problema: o contêiner permanece em starting e nunca fica healthy

  1. verifique se start_period é curto demais → tente 60s;
  2. confira se o comando do healthcheck está correto → use docker inspect para ver o comando executado;
  3. entre no contêiner e execute o comando manualmente → docker exec -it myapp_db_1 pg_isready -U postgres.

Problema: o contêiner alterna entre unhealthy e healthy

  1. interval pode estar curto demais para os recursos disponíveis → aumente para 10s ou 15s;
  2. retries pode estar baixo demais e uma falha eventual já dispara o estado → aumente para 5;
  3. o banco pode realmente ter um problema de desempenho → consulte os logs do banco.

Problema: o healthcheck passa, mas a aplicação ainda não conecta ao banco

  1. confira a string de conexão → o host deve ser o nome do serviço, como db, não localhost;
  2. confira o mapeamento de portas → a comunicação entre contêineres usa a porta interna, como 5432, não a porta publicada;
  3. confira a rede → confirme que todos os serviços estão na mesma network.

Boas práticas para produção

1. Valores recomendados — configuração conservadora

healthcheck:
  interval: 10s          # Equilíbrio entre resposta e consumo de recursos
  timeout: 5s            # Tempo suficiente para o comando executar
  retries: 5             # Tolera falhas eventuais
  start_period: 60s      # Dá tempo suficiente para o banco iniciar

Esses parâmetros são estáveis na maioria dos cenários. Se o banco tiver muitos scripts de inicialização, aumente start_period para 120s.

2. Use uma política de restart

web:
  depends_on:
    db:
      condition: service_healthy
      restart: true  # Reinicia a aplicação quando o banco reiniciar
  restart: unless-stopped  # Reinicia automaticamente quando o contêiner encerrar

restart: true garante que, após uma atualização ou reinicialização do banco, os serviços dependentes também reiniciem e estabeleçam uma nova conexão.

3. Limites de recursos

Healthchecks também consomem recursos, embora pouco. Se o sistema estiver no limite:

healthcheck:
  interval: 30s  # Aumenta o intervalo entre verificações
  timeout: 3s    # Reduz o timeout

Na prática, o custo do healthcheck costuma ser insignificante, a menos que você execute centenas de contêineres.

4. Monitore o estado de saúde

Em produção, recomenda-se acompanhar o estado dos healthchecks com uma ferramenta de monitoramento. Eventos de saúde do Docker podem ser coletados por ferramentas como Prometheus e Grafana.

# docker-compose.yml
services:
  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 60s
    labels:
      - "prometheus.io/scrape=true"  # Permite que o Prometheus colete o estado

5. Configurações por ambiente

Desenvolvimento e produção podem usar valores diferentes:

# docker-compose.yml (desenvolvimento)
db:
  healthcheck:
    start_period: 30s  # Poucos dados no desenvolvimento, inicialização rápida

# docker-compose.prod.yml (produção)
db:
  healthcheck:
    start_period: 120s  # Mais dados em produção, inicialização lenta
    interval: 5s        # Verificações mais frequentes

Ao iniciar, informe os arquivos de configuração:

docker-compose -f docker-compose.yml -f docker-compose.prod.yml up

Técnicas de depuração

Técnica 1: teste o comando do healthcheck manualmente

Entre no contêiner e execute o comando para descobrir o erro exato:

$ docker exec -it myapp_db_1 sh
/# pg_isready -U postgres -d myapp
/var/run/postgresql:5432 - accepting connections
/# echo $?
0 Código 0 indica sucesso

Técnica 2: desative o healthcheck temporariamente

Durante a depuração, comente o healthcheck e use um depends_on simples para descartar um problema na própria verificação:

web:
  depends_on:
    - db  # Modo simples temporário
    # db:
    #   condition: service_healthy

Depois de confirmar que a aplicação conecta normalmente ao banco, reative o healthcheck.

Técnica 3: consulte o log de eventos do Docker

O Docker registra todos os eventos dos contêineres, incluindo mudanças no estado de saúde:

$ docker events --filter 'event=health_status'

2024-12-17T03:15:30.123456789Z container health_status: healthy (name=myapp_db_1)
2024-12-17T03:16:45.987654321Z container health_status: unhealthy (name=myapp_db_1)

Isso ajuda a identificar o momento em que o contêiner se tornou unhealthy e relacioná-lo aos timestamps dos logs.

Conclusão

Voltando à pergunta do início: por que a aplicação falha mesmo com depends_on configurado?

Porque ele, sozinho, não é suficiente.

Por padrão, depends_on controla apenas a inicialização do contêiner, não a disponibilidade do serviço. Um contêiner de banco no estado running ainda pode não aceitar conexões, e essa diferença é a origem do problema.

A solução é direta:

  1. configure um healthcheck no banco, usando pg_isready ou mysqladmin ping para testar a disponibilidade real;
  2. use a condição service_healthy para a aplicação aguardar a aprovação do healthcheck;
  3. defina um start_period razoável, começando por 60 segundos, para dar tempo suficiente à inicialização do banco.

Com essas três medidas, as falhas de inicialização dos contêineres praticamente desaparecem.

Para colocar em prática:

  • agora: copie a configuração de PostgreSQL ou MySQL do artigo para seu projeto e ajuste as variáveis de ambiente;
  • hoje: atualize os depends_on dos projetos da equipe para service_healthy;
  • na próxima reunião: compartilhe a solução e padronize a configuração da equipe.

Se você encontrou um caso não abordado aqui, deixe um comentário. O Docker Compose tem muitas armadilhas, e podemos resolvê-las juntos.

Por fim, se este artigo ajudou a corrigir um problema antigo de inicialização, deixe sua curtida. É esse momento de “finalmente funcionou” que motiva a produção deste conteúdo.

Fluxo completo para configurar healthcheck no Docker Compose

Resolva falhas de inicialização causadas por um banco de dados ainda indisponível, com modelos completos para PostgreSQL e MySQL.

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Entenda a limitação de depends_on

    A raiz do problema:
    • O depends_on do Docker controla apenas a ordem de inicialização dos contêineres, não se o serviço está realmente pronto.
    • O Compose espera o contêiner iniciar, mas não verifica se o serviço já pode ser usado.
    • A documentação oficial do Docker diz: Compose does not wait until a container is "ready", only until it's running.
    • Em outras palavras: o Compose espera o contêiner iniciar, não o serviço ficar disponível.

    Diferença entre iniciar o contêiner e disponibilizar o serviço:
    • 0 segundo: o Docker inicia o contêiner e o processo postgres; depends_on já libera a aplicação.
    • 2 segundos: inicialização do diretório de dados.
    • 5 segundos: carregamento dos arquivos de configuração.
    • 8 segundos: execução dos scripts de init, se houver.
    • 12 segundos: o banco finalmente fica pronto para aceitar conexões.

    Há uma diferença de 12 segundos. Se a aplicação web tentar conectar no primeiro segundo, o resultado será Connection refused.
  2. 2

    Step 2: Conheça as três condições de depends_on

    As três condições de depends_on:

    1. service_started (padrão)
    • Espera apenas o contêiner iniciar, sem verificar se o serviço está pronto.
    • Não é recomendada para esse caso.

    2. service_healthy (recomendada)
    • Espera o contêiner iniciar e o healthcheck passar.
    • Garante que o serviço esteja realmente pronto.

    3. service_completed_successfully
    • Espera o contêiner concluir a execução e encerrar com sucesso.
    • É adequada para tarefas executadas uma única vez.

    Use service_healthy:
    • No docker-compose.yml, configure depends_on: db: condition: service_healthy.
    • Assim, a aplicação só inicia quando o banco de dados estiver realmente pronto.
  3. 3

    Step 3: Configure corretamente o healthcheck do PostgreSQL e do MySQL

    Healthcheck do PostgreSQL:
    • Use o comando pg_isready para verificar se o banco está pronto.
    • Exemplo:
    healthcheck:
    test: ["CMD-SHELL", "pg_isready -U postgres"]
    interval: 5s
    timeout: 5s
    retries: 5
    start_period: 60s (tempo suficiente para inicializar o banco)

    Healthcheck do MySQL:
    • Use mysqladmin ping para verificar se o banco está pronto.
    • Exemplo:
    healthcheck:
    test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$MYSQL_ROOT_PASSWORD"]
    interval: 5s
    timeout: 5s
    retries: 5
    start_period: 60s

    Configuração completa:
    • Defina o healthcheck no docker-compose.yml, incluindo comando, intervalo, timeout e número de tentativas.
    • Use a condição service_healthy em depends_on.
    • Garanta que a aplicação só inicie quando o banco estiver realmente pronto.
  4. 4

    Step 4: Faça o diagnóstico e aplique as boas práticas

    Checklist de diagnóstico:
    1. Verifique se o comando do healthcheck está correto.
    • Execute o comando manualmente dentro do contêiner e confirme que o código de saída é 0.
    • Exemplo: docker exec -it db_container pg_isready -U postgres.

    2. Desative o healthcheck temporariamente.
    • Durante a depuração, comente o healthcheck.
    • Use o depends_on simples para descartar um problema no próprio healthcheck.

    3. Consulte os eventos do Docker.
    • Use docker events para acompanhar eventos de inicialização e de healthcheck.

    Boas práticas:
    1. Configure healthcheck no banco com pg_isready ou mysqladmin ping para testar a disponibilidade real.
    2. Use a condição service_healthy para a aplicação aguardar a aprovação do healthcheck.
    3. Defina um start_period razoável, começando por 60 segundos, para dar tempo ao banco inicializar.

    Com essas três medidas, as falhas de inicialização dos contêineres praticamente desaparecem.

    Uma alternativa moderna aos scripts wait-for-it:
    • Use a combinação depends_on + healthcheck.
    • Não exige scripts adicionais, tem configuração simples e boa confiabilidade.
    • É adequada para projetos Docker Compose.

FAQ

Por que depends_on não é suficiente? Qual é a diferença entre o contêiner iniciar e o serviço ficar pronto?
O depends_on do Docker controla apenas a ordem de inicialização dos contêineres, não se o serviço está realmente pronto. O Compose espera o contêiner iniciar, mas não verifica se o serviço já pode ser usado.

A documentação oficial do Docker traz uma observação importante: Compose does not wait until a container is "ready", only until it's running. Ou seja, o Compose espera o contêiner iniciar, não o serviço ficar disponível.

Imagine o processo de inicialização de um contêiner PostgreSQL:
• 0 segundo: o Docker inicia o contêiner e o processo postgres; depends_on já libera a aplicação.
• 2 segundos: inicialização do diretório de dados.
• 5 segundos: carregamento dos arquivos de configuração.
• 8 segundos: execução dos scripts de init, se houver.
• 12 segundos: o banco finalmente fica pronto para aceitar conexões.

Há uma diferença de 12 segundos. Se a aplicação web tentar conectar no primeiro segundo, o resultado será Connection refused.
Quais são as três condições disponíveis em depends_on?
As três condições de depends_on são:

1) service_started (padrão):
• Espera apenas o contêiner iniciar, sem verificar se o serviço está pronto.
• Não é recomendada para esse caso.

2) service_healthy (recomendada):
• Espera o contêiner iniciar e o healthcheck passar.
• Garante que o serviço esteja realmente pronto.
• No docker-compose.yml, configure depends_on: db: condition: service_healthy.

3) service_completed_successfully:
• Espera o contêiner concluir a execução e encerrar com sucesso.
• É adequada para tarefas executadas uma única vez.

Recomenda-se service_healthy para iniciar a aplicação somente quando o banco estiver realmente pronto.
Como configurar o healthcheck do PostgreSQL e do MySQL?
PostgreSQL:
• Use pg_isready para verificar se o banco está pronto.
• Exemplo:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
start_period: 60s (tempo suficiente para inicializar o banco)

MySQL:
• Use mysqladmin ping para verificar se o banco está pronto.
• Exemplo:
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$MYSQL_ROOT_PASSWORD"]
interval: 5s
timeout: 5s
retries: 5
start_period: 60s

Defina o healthcheck no docker-compose.yml com comando, intervalo, timeout e tentativas, e use service_healthy em depends_on para que a aplicação só inicie quando o banco estiver pronto.
Como diagnosticar problemas no healthcheck?
Checklist de diagnóstico:

1) Verifique se o comando do healthcheck está correto:
• Execute o comando manualmente dentro do contêiner e confirme que o código de saída é 0.
• Exemplo: docker exec -it db_container pg_isready -U postgres.
• O código 0 indica sucesso.

2) Desative o healthcheck temporariamente:
• Durante a depuração, comente o healthcheck.
• Use o depends_on simples para descartar um problema no próprio healthcheck.
• Depois de confirmar que a aplicação conecta ao banco, reative o healthcheck.

3) Consulte os eventos do Docker:
• Use docker events para acompanhar eventos de inicialização e mudanças no healthcheck.
Quais são as boas práticas para configurar healthchecks?
Boas práticas:
1) Configure healthcheck no banco com pg_isready ou mysqladmin ping para testar a disponibilidade real.
2) Use a condição service_healthy para a aplicação aguardar a aprovação do healthcheck.
3) Defina um start_period razoável, começando por 60 segundos, para dar tempo ao banco inicializar.

Com essas três medidas, as falhas de inicialização dos contêineres praticamente desaparecem.

Uma alternativa moderna aos scripts wait-for-it:
• Use a combinação depends_on + healthcheck.
• Não exige scripts adicionais, tem configuração simples e boa confiabilidade.
• É adequada para projetos Docker Compose.

Ações rápidas:
• Agora: copie a configuração de PostgreSQL ou MySQL do artigo para seu projeto e ajuste as variáveis de ambiente.
• Hoje: atualize os depends_on dos projetos da equipe para service_healthy.
• Na próxima reunião: compartilhe a solução e padronize a configuração da equipe.

18 min de leitura · Publicado em: 17 dez 2025 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog