Alternar tema

Docker Compose em produção: health checks, políticas de reinicialização e gerenciamento de logs

Easton editorial illustration: observability control panel

Os alertas do servidor não paravam de chegar ao meu celular. Abri o terminal e vi o disco com 99% de uso — os logs dos contêineres ocupavam 50 GB.

E esse nem foi o pior caso. No ano passado, em um projeto, o contêiner da API aparecia como “running”, mas a conexão com o banco de dados já tinha caído e todas as requisições retornavam 500. Foram três horas inteiras de investigação até encontrarmos o problema. Segundo dados de uma pesquisa da Last9, um contêiner que parece ativo, mas não responde, consome em média 3,2 horas de diagnóstico a cada ocorrência.

Muitas equipes, na primeira implantação do Docker Compose em produção, configuram apenas o mapeamento de portas e os volumes e colocam os contêineres no ar. Não configuram health checks nem rotação de logs e escolhem restart: always sem pensar muito. O resultado é este: o contêiner parece estar em execução, embora o serviço já tenha parado; os arquivos de log crescem sem controle até ocupar todo o disco; e um serviço com falha reinicia indefinidamente, consumindo CPU e memória.

Este artigo explica em detalhes três configurações essenciais para produção: health checks, políticas de reinicialização e gerenciamento de logs. Além de exemplos de configuração, você encontrará comandos de verificação para serviços comuns, etapas de diagnóstico e um modelo completo de docker-compose.yml pronto para copiar.

Health checks — mantenha o contêiner realmente ativo

O status running não significa que a aplicação esteja funcionando de verdade. O banco de dados pode estar inacessível, a porta pode não estar escutando ou o processo pode ter travado — e o Docker não sabe disso sozinho. O health check funciona como um monitor de batimentos do contêiner, verificando periodicamente se a aplicação ainda responde normalmente.

Sintaxe de configuração

No docker-compose.yml, a configuração de healthcheck tem este formato:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres"]
  interval: 10s      # Verifica a cada 10 segundos
  timeout: 5s        # Aguarda no máximo 5 segundos por verificação
  retries: 5         # Marca como unhealthy após 5 falhas consecutivas
  start_period: 30s  # Reserva 30 segundos para o contêiner aquecer após iniciar

Esses parâmetros precisam funcionar em conjunto. O timeout não deve ser maior que o interval; caso contrário, a próxima verificação começará antes que a anterior termine. Também não ignore o start_period: serviços como bancos de dados demoram mais para iniciar, e um período de aquecimento curto demais pode fazer o health check concluir, incorretamente, que o contêiner falhou.

Comandos de health check para serviços comuns

Cada serviço exige uma forma diferente de verificação. Veja alguns exemplos frequentes.

PostgreSQL

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres -d mydb"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s

O pg_isready é uma ferramenta incluída no PostgreSQL e serve especificamente para verificar se o banco de dados está pronto para aceitar conexões.

MySQL / MariaDB

healthcheck:
  test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s

Observe que a senha usa $$ como escape; caso contrário, o YAML interpretará $ como uma referência de variável.

Redis

healthcheck:
  test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
  interval: 10s
  timeout: 3s
  retries: 3

O comando ping do Redis retorna PONG; o grep filtra a saída para confirmar que o resultado está correto.

Servidor web (verificação HTTP)

healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 10s

O parâmetro -f faz o curl retornar um código de saída diferente de zero quando o status HTTP não é 2xx, provocando a falha do health check.

Atenção a uma armadilha: imagens enxutas, como Alpine, podem não incluir curl. Você pode instalá-lo com apk add curl ou usar wget:

test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1"]

Controle da ordem de inicialização

O banco de dados ainda não está pronto, mas o contêiner da API já inicia, não consegue se conectar, registra um erro e encerra — já vi isso acontecer muitas vezes. A combinação de depends_on com condition: service_healthy resolve esse problema:

services:
  postgres:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  api:
    build: ./api
    depends_on:
      postgres:
        condition: service_healthy  # Inicia somente após o health check do postgres ser aprovado

Com isso, o Docker Compose aguarda o health check do postgres retornar healthy antes de iniciar o contêiner api. Assim, a API não tenta mais se conectar antes que o banco de dados esteja pronto.

Política de reinicialização — recupere-se de falhas com controle

O que fazer quando um contêiner falha? Reiniciá-lo automaticamente parece uma boa ideia. O problema é que, se a causa da falha não for resolvida, a reinicialização entra em um loop infinito, desperdiça CPU e memória e ainda esconde a falha real.

Sintaxe de configuração

A política de reinicialização é configurada no bloco deploy:

deploy:
  restart_policy:
    condition: on-failure   # Reinicia somente em caso de falha
    delay: 5s               # Aguarda 5 segundos antes de reiniciar
    max_attempts: 3         # Tenta reiniciar no máximo 3 vezes
    window: 120s            # Só considera a recuperação bem-sucedida após 120 segundos

Há três opções para condition:

  • none: não reinicia; se o contêiner parar, continuará parado
  • on-failure: reinicia apenas quando o contêiner termina de forma anormal, com código de saída diferente de 0
  • any: reinicia em qualquer situação

Recomendação para produção

Em produção, prefira on-failure a always.

Por quê? restart: always reinicia o contêiner em qualquer situação. Um bug no código derrubou a aplicação? Reinicia. O processo encerrou porque não conseguiu acessar o banco de dados? Reinicia. Um erro no arquivo de configuração impediu a inicialização? Reinicia de novo. O resultado é um loop de falhas, logs crescendo sem parar e consumo repetido de CPU.

Com on-failure e max_attempts, a situação muda: o contêiner tenta reiniciar no máximo três vezes e, se continuar falhando, para. A equipe de operações consegue perceber que ele encerrou definitivamente e investigar a causa real.

Ajuste dos parâmetros

delay é o intervalo entre reinicializações. Se for curto demais, o contêiner pode iniciar novamente antes que a limpeza anterior tenha terminado; se for longo demais, aumenta o tempo de recuperação. Em geral, de 5 a 10 segundos é uma faixa adequada.

O parâmetro window costuma ser ignorado. Ele define por quanto tempo o contêiner precisa permanecer sem falhar após a reinicialização para que ela seja considerada bem-sucedida. Por exemplo, com window: 120s, se o contêiner parar novamente dentro de 120 segundos, a contagem de max_attempts não será zerada. Isso evita considerar como sucesso um processo que reiniciou e falhou de novo um segundo depois.

Como health checks e política de reinicialização trabalham juntos

Health checks e políticas de reinicialização não funcionam de forma isolada; eles atuam em conjunto:

  1. O health check falha retries vezes seguidas → o contêiner é marcado como unhealthy
  2. Se houver uma restart_policy configurada, o Docker tentará reiniciar o contêiner
  3. Após a reinicialização, a contagem do health check começa novamente
  4. Se o health check for aprovado, o contêiner volta ao normal; se continuar falhando, haverá novas tentativas até esgotar max_attempts

Esse fluxo permite a recuperação automática de falhas e, ao mesmo tempo, limita o risco de reinicializações infinitas.

Gerenciamento de logs — evite ocupar todo o disco

O alerta das três da manhã mencionado no início — disco com 99% de uso e 50 GB de logs — já aconteceu comigo mais de uma vez. O driver de logs padrão do Docker, json-file, não remove automaticamente os logs antigos, então o arquivo cresce indefinidamente. Sem rotação de logs, mais cedo ou mais tarde o disco ficará cheio.

Configuração da rotação de logs

Adicione a configuração logging ao docker-compose.yml:

logging:
  driver: "json-file"
  options:
    max-size: "10m"      # Tamanho máximo de 10 MB por arquivo de log
    max-file: "3"        # Mantém no máximo 3 arquivos de log
    compress: "true"     # Compacta logs antigos para economizar espaço

Com essa configuração, os logs do contêiner ocupam no máximo 30 MB (10 MB × 3). Ao ultrapassar 10 MB, o Docker cria um novo arquivo; quando há mais de três arquivos, o mais antigo é removido ou compactado.

Os arquivos de log ficam em /var/lib/docker/containers/<container-id>/<container-id>-json.log. Use o comando du para verificar o espaço ocupado:

du -sh /var/lib/docker/containers/*/*-json.log

Escolha do driver

O Docker oferece vários drivers de logs: json-file, syslog, fluentd, journald, local e outros. Para a maioria dos cenários, json-file ou local são suficientes.

A documentação oficial do Docker informa que o driver local é mais eficiente que json-file e já inclui rotação de logs, sem exigir a configuração manual de max-size/max-file. Se o volume de logs for muito alto, como dezenas de GB por dia, considere usar local:

logging:
  driver: "local"

O driver local, porém, tem uma desvantagem: não permite consultar diretamente o conteúdo com docker logs. Para manter a compatibilidade, é necessário adicionar mode: "non-blocking" à configuração.

Coleta centralizada de logs (opcional)

Em uma implantação de servidor único, json-file ou local são suficientes. Porém, com dezenas de servidores e centenas de contêineres, gerenciar logs espalhados se torna difícil. Nesse caso, considere uma solução centralizada:

  • Fluentd: coleta de logs leve, adequada a clusters pequenos
  • ELK Stack (Elasticsearch + Logstash + Kibana): solução poderosa, mas com alto custo de implantação
  • Loki + Grafana: opção nativa da nuvem, bem integrada ao ecossistema Prometheus

Essas soluções exigem configurações mais complexas e estão fora do escopo deste artigo. Como exemplo, esta é a ideia básica de uma configuração com Fluentd:

logging:
  driver: "fluentd"
  options:
    fluentd-address: "localhost:24224"
    tag: "docker.{{.Name}}"

O Fluentd encaminha os logs ao endereço configurado, permitindo coletá-los e analisá-los de forma unificada em outro servidor.

Modelo completo de configuração

Ao combinar health checks, políticas de reinicialização e gerenciamento de logs, você obtém um docker-compose.yml adequado para produção. O exemplo completo abaixo inclui um banco de dados PostgreSQL, um cache Redis e um serviço de API:

version: '3.8'

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
        compress: "true"

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 5s
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      DATABASE_URL: postgres://myuser:mypassword@postgres:5432/mydb
      REDIS_URL: redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s
    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 3
        window: 120s
    logging:
      driver: "json-file"
      options:
        max-size: "50m"
        max-file: "5"
        compress: "true"

volumes:
  postgres_data:

Principais pontos da configuração

Ordem de inicialização: o depends_on do contêiner api aguarda simultaneamente a aprovação dos health checks de postgres e redis. A API só inicia quando o banco de dados e o cache estão prontos, evitando erros de conexão durante a inicialização.

Diferença no tamanho dos logs: postgres e redis geralmente geram poucos logs, então 10 MB × 3 é suficiente; o serviço de API pode gerar mais e usa 50 MB × 5. Ajuste os valores ao volume real de logs, em vez de aplicar o mesmo limite a todos.

Diferença no atraso de reinicialização: o postgres demora para iniciar e precisa de tempo para se recuperar após uma reinicialização, por isso o delay é de 5 segundos; a API inicia rapidamente, mas usa um delay de 10 segundos para dar mais margem ao health check.

Período de aquecimento: start_period: 30s para o postgres, dando tempo suficiente à inicialização do banco; start_period: 5s para o redis, que inicia rapidamente; e start_period: 10s para a API, pois alguns segundos costumam bastar para iniciar a aplicação.

Você pode copiar esse modelo diretamente e substituir apenas as variáveis de ambiente e as imagens pelas do seu projeto. Se houver outros serviços, como MongoDB ou MinIO, siga o mesmo padrão e acrescente health checks, políticas de reinicialização e configurações de logs.

Armadilhas comuns e diagnóstico

Mesmo depois de concluir a configuração e a implantação, ainda podem surgir problemas. Veja algumas armadilhas comuns e como investigá-las.

O health check sempre falha

Sintoma: o status do contêiner permanece como unhealthy, embora a aplicação pareça funcionar normalmente.

Etapas de diagnóstico:

  1. Primeiro, verifique se a ferramenta usada pelo comando de health check existe:

    docker exec <container> which curl
    docker exec <container> which pg_isready

    Imagens Alpine frequentemente não incluem curl; instale-o manualmente ou use wget.

  2. Execute manualmente o comando de health check e confira a saída:

    docker exec <container> curl -f http://localhost:8080/health

    Se houver erro, o próprio endpoint de health check pode estar com problema.

  3. Consulte o estado detalhado do health check:

    docker inspect --format='{{json .State.Health}}' <container> | jq

    A saída mostra os resultados das verificações recentes, as causas das falhas e os horários.

O contêiner reinicia repetidamente

Sintoma: o contêiner inicia, encerra alguns segundos depois e os logs ficam cheios de registros de reinicialização.

Etapas de diagnóstico:

  1. Verifique a causa da saída do contêiner:

    docker inspect --format='{{.State.ExitCode}}' <container>
    docker inspect --format='{{.State.Error}}' <container>

    O código de saída indica aproximadamente o tipo de problema (1 = erro geral, 137 = processo encerrado por OOM, 139 = falha de segmentação).

  2. Consulte a quantidade de reinicializações:

    docker inspect --format='{{.RestartCount}}' <container>

    Se o número for muito alto, verifique se max_attempts está funcionando.

  3. Consulte os logs do contêiner para encontrar o erro específico:

    docker logs --tail 100 <container>

O disco ficou cheio por causa dos logs

Sintoma: o sistema alerta sobre pouco espaço em disco e o diretório /var/lib/docker/containers ocupa muito espaço.

Etapas de diagnóstico:

  1. Encontre os maiores arquivos de log:

    du -sh /var/lib/docker/containers/*/*-json.log | sort -rh | head -5
  2. Confira se a configuração de rotação de logs está funcionando:

    docker inspect --format='{{.HostConfig.LogConfig}}' <container>

    Se a saída mostrar Config: {}, a rotação de logs não foi configurada.

  3. Limpe os logs manualmente como solução temporária:

    truncate -s 0 /var/lib/docker/containers/<id>/<id>-json.log

    Essa é uma solução temporária. A correção permanente é configurar a rotação de logs.

Lista de comandos para diagnóstico rápido

Quando surgir um problema, estes comandos ajudam a identificar a causa rapidamente:

# Mostra o estado de saúde de todos os contêineres
docker ps --format "table {{.Names}}\t{{.Status}}"

# Mostra o histórico de health checks de um contêiner
docker inspect --format='{{json .State.Health}}' <container>

# Mostra o código de saída e a quantidade de reinicializações
docker inspect --format='ExitCode: {{.State.ExitCode}}, RestartCount: {{.RestartCount}}' <container>

# Mostra o tamanho dos arquivos de log
du -sh /var/lib/docker/containers/*/*-json.log | sort -rh

# Mostra as 100 entradas de log mais recentes do contêiner
docker logs --tail 100 <container>

Conclusão

Ao implantar o Docker Compose em produção, estas três configurações não são opcionais, mas essenciais: health checks fazem o contêiner mais do que apenas parecer ativo; políticas de reinicialização dão ao serviço uma chance de se recuperar automaticamente sem entrar em um loop infinito; e o gerenciamento de logs evita que o disco fique cheio.

3,2 horas
Tempo médio para diagnosticar um contêiner que parece ativo, mas não responde

Checklist das configurações essenciais:

  • Health check: test + interval + timeout + retries + start_period
  • Política de reinicialização: condition: on-failure + max_attempts: 3
  • Rotação de logs: max-size: 10m + max-file: 3 + compress: true

Plano de ação em três etapas:

  1. Verifique seu docker-compose.yml atual e confirme se as três configurações estão presentes. Se alguma faltar, adicione pelo menos o health check e a rotação de logs.
  2. Use o modelo completo acima para implantar um serviço de teste e observe se o health check funciona e se os logs estão sendo rotacionados.
  3. Guarde os comandos de diagnóstico. Na próxima vez que um alerta chegar às três da manhã, você poderá localizar rapidamente o problema.

Não deixe seus contêineres desprotegidos em produção. Configure essas três camadas de proteção para que o serviço possa se recuperar automaticamente, o diagnóstico seja rápido e os logs não ocupem todo o disco.

FAQ

Como definir valores adequados para interval e timeout no health check?
Recomenda-se um interval de 10 a 30 segundos e um timeout de 3 a 10 segundos. O ponto principal é que timeout não deve ser maior que interval, pois a próxima verificação poderia começar antes que a anterior terminasse. Para serviços de banco de dados, use um start_period mais longo, de 30 a 60 segundos, para reservar tempo à inicialização.
É melhor usar restart: always ou on-failure?
Em produção, recomenda-se on-failure combinado com max_attempts. A opção always reinicia o contêiner em qualquer situação, inclusive em caso de erro de configuração ou bug no código, o que pode causar um loop de falhas. Já on-failure só reinicia após uma saída anormal e, com max_attempts, limita o número de tentativas para que a equipe de operações perceba o problema.
Qual deve ser o tamanho dos arquivos de log e quantos devem ser mantidos?
Para serviços comuns, uma boa referência é max-size: 10m + max-file: 3, totalizando 30 MB. Serviços que geram muitos logs, como APIs, podem usar 50m × 5. O importante é ajustar os valores ao volume real de logs e ativar compress: true para economizar espaço.
O health check do contêiner sempre falha, mas a aplicação funciona normalmente. O que fazer?
Primeiro, confirme se a ferramenta usada pelo comando de verificação existe na imagem, como curl ou pg_isready; imagens Alpine frequentemente não incluem essas ferramentas. Depois, execute o comando manualmente para conferir a saída e, por fim, use docker inspect para consultar o histórico do health check e identificar a causa específica da falha.
Para que serve condition: service_healthy em depends_on?
Essa condição faz o contêiner atual iniciar somente depois que o health check do serviço dependente for aprovado. É mais confiável que um depends_on simples e evita que a API tente se conectar antes que o banco de dados esteja pronto, causando uma falha na inicialização. Para funcionar, o serviço dependente precisa ter um healthcheck configurado.
Como verificar rapidamente quanto espaço em disco os logs dos contêineres ocupam?
Use o comando: du -sh /var/lib/docker/containers/*/*-json.log | sort -rh. Se o log de um único contêiner estiver muito grande, confira se a configuração de rotação de logs desse contêiner está funcionando.

14 min de leitura · Publicado em: 12 abr 2026 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog