Alternar tema

Três pilares do Docker Compose em produção: healthcheck, política de reinicialização e limites de recursos

Easton editorial illustration: input-process-output transport line

O alerta do servidor chegou por SMS e me arrancou da cama.

Abri o computador e vi que o contêiner aparecia como running, com aquele pequeno indicador verde que parecia perfeitamente saudável. Mas, ao acessar o serviço, recebi um 502 Bad Gateway.

O contêiner do banco de dados ainda não havia terminado de iniciar, mas o da aplicação já tentava se conectar. A conexão falhou e o serviço caiu. O contêiner continuava “em execução”, embora o serviço já estivesse morto.

Foi isso que aconteceu na primeira vez em que implantei o Docker Compose em produção.

Conseguir executar e executar com estabilidade são coisas completamente diferentes. O fato de docker-compose up iniciar tudo com um único comando não significa que a aplicação continuará de pé quando houver um estouro de memória ou uma falha de processo no meio da madrugada.

Os três pilares do Docker Compose em produção — healthcheck, política de reinicialização e limites de recursos — existem justamente para resolver esse problema. Neste artigo, compartilho as configurações que consolidei depois de enfrentar essas situações, além de um modelo YAML pronto para copiar. A ideia é transformar seus contêineres de algo que apenas “funciona” em algo que “funciona com estabilidade”.

1. Healthcheck: verifique se o contêiner está realmente disponível

O status running mostrado por docker ps informa apenas que o processo do contêiner continua em execução. Ele consegue atender às requisições normalmente? Não dá para saber só por esse status.

O healthcheck permite que o Docker faça uma “verificação periódica” do contêiner: envia uma requisição HTTP, tenta se conectar ao banco de dados ou executa um script para descobrir se o serviço está realmente ativo.

Como o healthcheck funciona?

No intervalo definido por você, o Docker envia um comando de verificação para o contêiner. Se o comando retornar 0, ele está saudável; se retornar um valor diferente de 0, não está. Depois de várias falhas consecutivas, o contêiner recebe o status unhealthy.

O ponto principal é: uma falha no healthcheck não dispara uma reinicialização automática. Ela apenas expõe o estado e avisa que “há algo errado com este serviço”. Para recuperar o serviço automaticamente, você precisa combinar o healthcheck com a inicialização condicional de depends_on e uma política de reinicialização.

Quatro parâmetros que você precisa entender

healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
  interval: 30s      # Intervalo entre as verificações
  timeout: 10s       # Tempo limite de cada verificação
  retries: 3         # Falhas consecutivas antes de marcar como unhealthy
  start_period: 60s  # Período de tolerância; falhas aqui não contam em retries

Já ignorei o parâmetro start_period e o resultado foi este: a aplicação demorava para iniciar e levava 40 segundos para se conectar ao banco de dados, mas o healthcheck começava aos 10 segundos. Depois de três falhas consecutivas, o contêiner era marcado imediatamente como unhealthy. Com start_period: 60s, a aplicação passou a ter tempo suficiente para concluir a inicialização.

Comandos comuns de healthcheck

Serviço Web:

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

PostgreSQL:

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

Redis:

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

Combine com depends_on para controlar a ordem de inicialização

Essa é a aplicação mais útil do healthcheck: fazer um serviço esperar até que suas dependências estejam realmente prontas antes de iniciar.

services:
  app:
    depends_on:
      db:
        condition: service_healthy  # Aguarda o healthcheck do banco de dados
      redis:
        condition: service_healthy  # Aguarda o healthcheck do Redis

Eu usava depends_on: [db, redis]. O contêiner da aplicação iniciava enquanto o banco de dados ainda estava sendo preparado, não conseguia se conectar e encerrava com erro. Depois de trocar para condition: service_healthy, a aplicação passou a esperar tranquilamente até que o banco de dados respondesse ao pg_isready. Problema resolvido.

2. Política de reinicialização (restart): dê autorrecuperação ao contêiner

Quando o contêiner cai, quem o coloca de pé novamente?

Executar docker restart manualmente? Tente fazer isso quando o alerta chegar às três da manhã.

A política de reinicialização delega essa tarefa ao daemon do Docker. Depois que o contêiner termina, o Docker decide automaticamente se deve reiniciá-lo.

Comparação entre as quatro políticas

PolíticaComportamentoQuando usar
noEncerra e não reiniciaTestes temporários, CI/CD
alwaysReinicia independentemente da forma de encerramentoServiços essenciais
on-failureReinicia apenas após uma saída com erroContêineres de tarefas
unless-stoppedSempre reinicia, exceto após uma parada manualPreferida em produção

A melhor opção para produção: unless-stopped

restart: unless-stopped

Por que recomendar unless-stopped em vez de always?

A diferença está no comportamento depois de executar docker stop manualmente.

  • always: depois de uma parada manual, o contêiner volta a iniciar se o sistema ou o serviço do Docker for reiniciado
  • unless-stopped: depois de uma parada manual, o contêiner permanece realmente parado e não volta sozinho

Imagine parar um contêiner manualmente para fazer manutenção e, depois de reiniciar o servidor, descobrir que ele voltou a executar por conta própria. A reação natural seria perguntar: o que aconteceu aqui?

Limite de tentativas com on-failure

É possível definir o número de tentativas de on-failure:

restart: on-failure:5  # No máximo 5 reinicializações

Se o contêiner falhar ao iniciar cinco vezes seguidas, o Docker desiste. Essa configuração é adequada para situações em que fatores externos, como um banco de dados inacessível ou uma configuração incorreta, podem causar falhas repetidas. Assim, você evita um ciclo infinito de reinicializações.

Como escolher? Em resumo

  • Serviços essenciais (Web, API e banco de dados): unless-stopped
  • Tarefas em segundo plano e scripts agendados: on-failure
  • Desenvolvimento, depuração e execuções temporárias: no

Uma armadilha: a política de reinicialização só determina “se o contêiner deve reiniciar depois de encerrar”. Ela não verifica “se o serviço está realmente disponível”. Para cobrir os dois cenários, é preciso combiná-la com um healthcheck.

3. Limites de recursos (deploy.resources): evite que o contêiner saia de controle

Você já viu um contêiner com vazamento de memória consumir toda a RAM do servidor e fazer com que os demais fossem encerrados pelo OOM Killer?

Eu já. Não é uma experiência agradável.

Os limites de recursos definem um “teto” para cada contêiner: se ele ultrapassar esse limite, será encerrado, protegendo os outros serviços.

limits vs. reservations

deploy:
  resources:
    limits:
      cpus: '1.0'      # No máximo 1 CPU
      memory: 512M     # No máximo 512 MB de memória
    reservations:
      cpus: '0.5'      # Reserva mínima de 0,5 CPU
      memory: 256M     # Reserva mínima de 256 MB de memória
  • limits: limite rígido; se for ultrapassado, o processo é encerrado (OOM)
  • reservations: limite flexível; informa ao agendador que “este contêiner precisa de pelo menos esta quantidade de recursos”

Em termos simples, limits significa “não pode ultrapassar”, enquanto reservations significa “garanta pelo menos isto”.

Como definir o limite de CPU?

cpus: '1.0'   # Pode usar no máximo 1 núcleo de CPU por completo
cpus: '0.5'   # Pode usar no máximo 50% de uma CPU
cpus: '2.0'   # Pode usar no máximo 2 núcleos

O limite de CPU é flexível: ao ultrapassá-lo, o contêiner é desacelerado, não encerrado. Por isso, é melhor deixar alguma margem.

Como definir o limite de memória?

memory: 512M    # 512 MB
memory: 2G      # 2 GB

O limite de memória é rígido. Se for ultrapassado, o contêiner é encerrado pelo OOM Killer, sem negociação.

Valores que costumo usar:

  • Aplicações Node.js: pelo menos 512M; em produção, recomendo 1G
  • Aplicações Python: 256M - 512M
  • PostgreSQL: entre 1G - 4G, conforme o número de conexões e o volume de dados
  • Redis: 256M - 512M; um cache maior pode exigir mais memória

Uma configuração prática

services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
        reservations:
          cpus: '0.25'
          memory: 256M

Com essa configuração, a aplicação pode usar no máximo uma CPU e 1 GB de memória, enquanto o Docker garante pelo menos 0,25 CPU e 256 MB de memória.

Observação: a configuração deploy é voltada principalmente ao Docker Swarm. Em uma implantação de host único com docker-compose up, os limites de recursos funcionam com o Docker Compose V2 ou com o parâmetro docker-compose --compatibility. Uma opção mais comum em um único host é usar mem_limit e cpus — hoje obsoletos — ou adotar diretamente deploy, compatível com o Compose V2.20+.

4. Modelo completo: YAML pronto para produção e para copiar

Cada um dos três pilares já é útil por si só, mas é a combinação deles que forma uma configuração pronta para produção. A seguir, há um exemplo completo com um serviço Web, PostgreSQL e Redis, pronto para copiar e adaptar.

Exemplo completo

version: '3.8'

services:
  # Aplicação Web
  app:
    image: myapp:latest
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
        reservations:
          cpus: '0.25'
          memory: 256M
    logging:
      driver: json-file
      options:
        max-size: "10m"   # Máximo de 10 MB por arquivo de log
        max-file: "3"     # Mantém no máximo 3 arquivos de log

  # Banco de dados PostgreSQL
  db:
    image: postgres:15
    restart: unless-stopped
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: apppassword
      POSTGRES_DB: appdb
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 512M
    volumes:
      - pgdata:/var/lib/postgresql/data

  # Cache Redis
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          memory: 128M

volumes:
  pgdata:

Como separar as configurações

As configurações dos ambientes de desenvolvimento e produção costumam ser diferentes. Gerencie-as em dois arquivos separados:

# compose.yaml - Ambiente de desenvolvimento
version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    restart: "no"  # Não queremos reinicialização automática durante o desenvolvimento
# compose.production.yaml - Substituições para produção
version: '3.8'
services:
  app:
    image: myapp:v1.2.3  # Imagem já criada para produção
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 1G

Inicie o ambiente de produção:

docker-compose -f compose.yaml -f compose.production.yaml up -d

Os dois arquivos são combinados, e as configurações de compose.production.yaml sobrescrevem as de compose.yaml.

Gerenciamento de logs: evite ocupar todo o disco

O driver de logs padrão do Docker é json-file, e esses logs crescem indefinidamente. Sem limites, eles podem ocupar todo o disco depois de alguns meses.

logging:
  driver: json-file
  options:
    max-size: "10m"   # Máximo de 10 MB por arquivo
    max-file: "3"     # No máximo 3 arquivos, totalizando até 30 MB

Cada contêiner mantém no máximo 30 MB de logs, com rotação automática. Adiciono essa configuração a todos os serviços para não precisar fazer uma limpeza manual no futuro.

Conclusão

Depois de tudo isso, a lógica central dos três pilares é:

O healthcheck detecta o problema → a política de reinicialização recupera o serviço → os limites de recursos impedem que a falha se espalhe

Com essa combinação, sua aplicação no Docker Compose consegue:

  1. Esperar que as dependências estejam realmente prontas antes de iniciar, em vez de tentar avançar às cegas
  2. Recuperar-se sozinha após uma falha, sem tirar você da cama no meio da madrugada
  3. Impedir que um contêiner fora de controle derrube o servidor inteiro

Agora, confira seu docker-compose.yml. Qual dessas configurações está faltando? Adicione-a.

Se você ainda não usa o Docker Compose para implantar em produção, leve essas três configurações para a próxima implantação. Seu eu do futuro vai agradecer.

Configurar os três pilares de uma implantação do Docker Compose em produção

Adicione healthchecks, políticas de reinicialização e limites de recursos ao Docker Compose para criar uma implantação estável e pronta para produção

⏱️ Estimated time: 15 min

  1. 1

    Step 1: Adicionar a configuração de healthcheck

    Adicione uma configuração healthcheck a cada serviço:

    • test: comando de verificação (curl, pg_isready, redis-cli ping etc.)
    • interval: intervalo entre verificações (recomendado: 10 a 30s)
    • timeout: tempo limite (recomendado: 5 a 10s)
    • retries: número de falhas (recomendado: 3 a 5)
    • start_period: período de tolerância na inicialização (defina entre 30 e 60s, conforme o tempo de inicialização da aplicação)
  2. 2

    Step 2: Configurar a inicialização condicional dos serviços dependentes

    Use o parâmetro condition de depends_on:

    • Troque depends_on: [db] por depends_on: db: condition: service_healthy
    • Verifique se o healthcheck do serviço dependente está configurado
    • O serviço da aplicação aguardará até que a dependência esteja realmente disponível
  3. 3

    Step 3: Definir a política de reinicialização

    Escolha a política de reinicialização conforme o tipo de serviço:

    • Serviços essenciais (Web/API/banco de dados): restart: unless-stopped
    • Tarefas em segundo plano/scripts agendados: restart: on-failure:5
    • Desenvolvimento e depuração: restart: 'no'
    • Evite always (o contêiner pode reiniciar inesperadamente após uma parada manual)
  4. 4

    Step 4: Configurar limites de recursos

    Defina limits e reservations em deploy.resources:

    • limits: limite rígido; ao ultrapassá-lo, o processo é encerrado pelo OOM Killer
    • reservations: limite flexível; quantidade mínima de recursos garantida pelo Docker
    • Aplicações Node.js: recomenda-se pelo menos 512M em limits.memory
    • Bancos de dados: defina entre 1G e 4G de acordo com o número de conexões e o volume de dados
  5. 5

    Step 5: Adicionar rotação de logs

    Evite que os arquivos de log ocupem todo o disco:

    • logging.driver: json-file (driver padrão)
    • logging.options.max-size: '10m' (máximo de 10 MB por arquivo)
    • logging.options.max-file: '3' (mantém três arquivos)
    • Limite total de 30 MB de logs, com rotação automática

FAQ

O contêiner reinicia automaticamente quando o healthcheck falha?
Não. O healthcheck apenas marca o contêiner como unhealthy; ele não dispara uma reinicialização automaticamente. É necessário combiná-lo com uma política de reinicialização, como unless-stopped, e com a condição service_healthy de depends_on. A política de reinicialização só entra em ação quando o processo do contêiner falha e termina.
Qual é a diferença entre unless-stopped e always?
A principal diferença está no comportamento após uma parada manual: com a política always, depois de executar docker stop manualmente, o contêiner volta a iniciar se o servidor ou o serviço do Docker for reiniciado; com unless-stopped, o contêiner permanece parado após uma parada manual. Para produção, recomenda-se unless-stopped.
Qual é a diferença entre limits e reservations nos limites de recursos?
limits é um limite rígido: se o consumo de memória do contêiner ultrapassar limits.memory, ele será encerrado pelo OOM Killer. reservations é um limite flexível: informa ao agendador do Docker a quantidade mínima de recursos de que o contêiner precisa, mas ele ainda pode usar mais. O limite de CPU é flexível (o contêiner é desacelerado ao ultrapassá-lo), enquanto o de memória é rígido (o contêiner é encerrado).
Para que serve o parâmetro start_period?
start_period é o período de tolerância durante a inicialização. Falhas de healthcheck nesse intervalo não são contabilizadas em retries. Para uma aplicação que leva mais tempo para iniciar — por exemplo, 40 segundos para se conectar ao banco de dados —, definir start_period: 60s evita que ela seja marcada incorretamente como unhealthy logo ao iniciar.
Como usar configurações diferentes nos ambientes de desenvolvimento e produção?
Separe as configurações em dois arquivos:

• compose.yaml: configuração do ambiente de desenvolvimento (build: ., restart: 'no')
• compose.production.yaml: substituições para produção (image: xxx, restart: unless-stopped)
• Comando de inicialização: docker-compose -f compose.yaml -f compose.production.yaml up -d
• O segundo arquivo sobrescreve as configurações do primeiro, mantendo os ambientes separados
Posso usar deploy.resources em uma implantação de host único?
Sim. Embora a configuração deploy seja voltada principalmente ao Docker Swarm, o Docker Compose V2.20+ permite usá-la em um único host. Em versões antigas, é necessário adicionar o parâmetro --compatibility ou usar os parâmetros obsoletos mem_limit/cpus. Recomenda-se atualizar para a versão mais recente do Docker Compose.

10 min de leitura · Publicado em: 24 abr 2026 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog