Sistema de Health Check

O health check e o mecanismo que o Runner usa para decidir se um deploy foi bem-sucedido ou se precisa de rollback. A partir da v2.1.0 o sistema foi reescrito para resolver problemas reais encontrados em deploys de producao.

Visao Geral do Fluxo

┌──────────────────────────────────────────────────────────────────┐
│                  runner deploy meu-app                            │
├──────────────────────────────────────────────────────────────────┤
│                                                                   │
│  1. Fetch codigo                                                  │
│  2. Build/Pull imagem                                             │
│  3. docker inspect .Config  ←─── Verifica HEALTHCHECK + VOLUMEs  │
│  4. Decide estrategia de health:                                  │
│     ┌────────────────────────────────────────────────────────┐   │
│     │ Imagem tem HEALTHCHECK?                                 │   │
│     │   SIM → Respeita. Nao injeta --health-cmd              │   │
│     │   NAO → Resolve modo do .deploy.yml (cmd > tcp > path) │   │
│     │         → Injeta --health-cmd no docker run             │   │
│     └────────────────────────────────────────────────────────┘   │
│  5. docker run (com ou sem --health-cmd)                          │
│  6. wait_for_healthy (poll a cada 2s ate timeout)                 │
│     ┌────────────────────────────────────────────────────────┐   │
│     │ healthy    → Deploy OK, remove container anterior       │   │
│     │ unhealthy  → Continua esperando (pode recuperar)        │   │
│     │ exited     → Bail, rollback                             │   │
│     │ timeout    → Bail, rollback                             │   │
│     └────────────────────────────────────────────────────────┘   │
│  7. Traefik routing                                               │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘

Os Tres Modos

O .deploy.yml suporta tres modos mutuamente exclusivos para o health check. Prioridade: cmd > tcp > path.

Modo HTTP (padrao)

O mais comum. O runner faz um GET HTTP no path indicado e espera status 2xx.

healthcheck:
  path: /health
  interval: 10s
  timeout: 5s
  retries: 3
  start_period: 30s

O que o runner injeta no docker run:

Image mode (imagem pronta do registry):

--health-cmd "sh -c '(curl -sf http://127.0.0.1:PORT/health || wget -q -O /dev/null http://127.0.0.1:PORT/health) 2>/dev/null'"

Tenta curl primeiro, fallback pra wget. Imagens Alpine geralmente tem wget (via busybox) mas nao curl.

Build mode (Dockerfile do repo):

--health-cmd "bash -c 'echo -e \"GET /health HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n\" > /dev/tcp/127.0.0.1/PORT'"

Usa /dev/tcp do bash, que funciona sem instalar nenhum binario extra. Mas requer bash — imagens alpine puras nao tem bash (tem sh/busybox).

Dica: se a imagem e minimal (alpine, distroless), declare HEALTHCHECK no Dockerfile ao inves de depender do runner injetar.

Modo TCP

Para servicos que nao falam HTTP (SMTP, Redis, AMQP, bancos de dados). Considera saudavel se a porta aceita conexao TCP.

healthcheck:
  tcp: 25
  interval: 10s
  timeout: 5s
  retries: 3

O runner injeta:

--health-cmd "sh -c 'nc -z 127.0.0.1 25 2>/dev/null || (exec 3<>/dev/tcp/127.0.0.1/25; exec 3<&-; exec 3>&-)'"

Tenta nc -z primeiro, fallback pra /dev/tcp.

Modo CMD

Para quando a imagem ja tem um binario de healthcheck ou voce quer controle total do teste.

healthcheck:
  cmd: ["/app/bin/healthcheck", "--strict"]
  interval: 10s
  timeout: 5s
  retries: 3

O runner passa o comando exatamente como esta (exec form). Nao depende de curl, wget, bash ou qualquer outra ferramenta.

Quando usar: imagens distroless, binarios Go/Rust com healthcheck embutido, checks que vao alem de HTTP (verificar conexao ao DB, checar fila, etc).

Heranca do HEALTHCHECK do Dockerfile

Esta e a mudanca mais importante da v2.1.0.

Se o Dockerfile da imagem (base ou buildada) ja declara HEALTHCHECK, o runner respeita e nao injeta --health-cmd nenhum.

# No seu Dockerfile
HEALTHCHECK --interval=10s --timeout=5s --retries=3 \
  CMD wget -q -O /dev/null http://127.0.0.1/health || exit 1

Nesse caso o runner:

  1. Faz docker inspect --format '{{json .Config}}' <imagem>
  2. Encontra Healthcheck.Test: ["CMD-SHELL", "wget ..."]
  3. Loga: Honoring HEALTHCHECK declared by image
  4. NAO passa --health-cmd ao docker run

Beneficios:

  • Voce testa o healthcheck localmente (docker build && docker run)
  • Nao depende de curl/wget/bash estar na imagem — voce escolhe
  • Funciona com imagens minimal (alpine, distroless, scratch)
  • Nao ha surpresa: o que roda em dev e o que roda em prod

Para ver qual modo o runner vai usar:

runner deploy meu-app -V
# Procure por:
#   "Honoring HEALTHCHECK declared by image" (heranca)
#   "Injecting healthcheck: sh -c '(curl ...'" (runner injeta)

Timing e Tolerancia

O runner calcula o timeout total a partir dos campos do .deploy.yml:

timeout = start_period + (interval + timeout) × retries + buffer(30s)

Com os defaults (start_period=30s, interval=10s, timeout=5s, retries=3):

30 + (10 + 5) × 3 + 30 = 105s

start_period (novo na v2.1.0)

Grace period antes do Docker comecar a rodar checks. Containers que demoram pra subir (multi-stage builds, JVMs, cold start de nginx) devem aumentar este valor.

healthcheck:
  path: /health
  start_period: 60s    # default: 30s
  interval: 10s
  retries: 5

Tolerancia a Unhealthy transitorio

Antes da v2.1.0, se o Docker marcava o container como unhealthy (retries esgotados), o runner abortava imediatamente com rollback — mesmo se o container estivesse prestes a ficar saudavel.

Agora o runner trata unhealthy como "tentando ainda" enquanto estiver dentro do timeout calculado:

t=0s      docker run
t=30s     start_period termina, Docker roda 1a check → falha
t=40s     2a check → falha
t=50s     3a check → falha → Docker marca "unhealthy"
t=50s     Runner ve "unhealthy" mas timeout=105s → continua esperando
t=60s     Docker roda proximo ciclo de checks → sucesso!
t=60s     Docker marca "healthy" → Runner aceita deploy ✓

Antes, o deploy falhava em t=50s. Agora espera ate t=105s.

Arvore de Decisao

Imagem ja tem HEALTHCHECK? (docker inspect)
  │
  ├── SIM → Honrar. Nao injetar nada.
  │         Runner apenas faz poll de status ate healthy/timeout.
  │
  └── NAO → Checar .deploy.yml:
            │
            ├── cmd: definido? → Usar cmd (exec form)
            │
            ├── tcp: definido? → Sondagem TCP (nc/dev-tcp)
            │
            ├── path: nao-vazio? → Probe HTTP
            │   │
            │   ├── Build mode → bash /dev/tcp
            │   └── Image mode → curl || wget
            │
            ├── mode: exit? → CLI tool (sem container permanente)
            │   fetch --deploy skipa, deploy manual valida com --rm
            │
            └── Nada definido → Sem healthcheck
                Runner aceita "running" como sucesso.

Healthcheck Type-Aware no Generator (v2.18.2+)

Quando o runner add ou runner wizard gera um .deploy.yml automaticamente (perfis B, C, D do generator), o healthcheck é escolhido baseado na imagem detectada no Dockerfile ou compose. Antes (v2.18.1) o default era TCP cego pra tudo, o que quebrava nginx/caddy (que escutam HTTP) e era subótimo pra php-fpm (que precisa TCP no FastCGI).

A heurística é simples — detect_default_healthcheck() casa o nome da imagem por prefixo:

Imagem detectada Healthcheck gerado
nginx*, caddy*, httpd*, apache*, traefik* mode: http + path: / + port: <EXPOSE>
php-fpm*, *-fpm*, *fpm* mode: tcp + tcp: <EXPOSE> (FastCGI, não HTTP)
node*, python*, ruby*, openjdk*, rust* mode: tcp + tcp: <EXPOSE> (genérico)
Qualquer outra mode: tcp + tcp: <EXPOSE> (fallback seguro)

Exemplo de geração pra repo Dockerfile FROM nginx:1 EXPOSE 8080:

healthcheck:
  mode: http
  path: /
  port: 8080
  interval: 30s
  timeout: 10s
  retries: 3

Pra php-fpm:8.2 EXPOSE 9000:

healthcheck:
  mode: tcp
  tcp: 9000
  interval: 30s
  ...

Sempre dá pra override: o user pode editar o .deploy.yml gerado e mudar pra qualquer outro modo. A heurística só decide o default inicial.

Por que path: / e não /health? Pra apps web genéricas (nginx, caddy, etc.) servindo conteúdo estático, / sempre existe. /health é convenção de apps Flask/Express/etc. — se o user tem esse endpoint, edita o yml.

Modo Exit (CLI Tools / Scanners)

Para aplicacoes que nao sao servicos (scanners, CLI tools, jobs one-shot), use mode: exit:

healthcheck:
  mode: exit

Comportamento:

Trigger Acao
fetch --deploy (cron) Skipa o deploy, so atualiza artefatos
runner deploy (manual) Builda/puxa imagem, valida com docker run --rm <image> --version (exit 0 = OK)

Caracteristicas:

  • Sem container permanente rodando
  • Sem rota Traefik
  • Imagem fica disponivel para docker run --rm sob demanda
  • Ideal para scanners, CLI tools, batch jobs

Quando usar:

Projeto Healthcheck Motivo
Frontend, Backend, API path: /health Servico que fica rodando
Scanner, CLI tool mode: exit Roda e sai, sem servico
Banco, Redis tcp: 3306 Servico sem HTTP
App com HC proprio cmd: ["/bin/check"] Imagem com HEALTHCHECK

Exemplos Praticos

Frontend React (Caddy, imagem pronta)

# .deploy.yml
project: meu-front
image: caddy:2-alpine
port: 80

healthcheck:
  path: /health
  interval: 10s
  timeout: 5s
  retries: 3

O Caddy em alpine nao tem curl, mas tem wget (busybox). O runner injeta curl || wget e o wget funciona.

Frontend com Dockerfile customizado

# .deploy.yml
project: meu-front
port: 80
build:
  context: .
  dockerfile: Dockerfile
# Dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM caddy:2-alpine
COPY --from=build /app/dist /srv
COPY Caddyfile /etc/caddy/Caddyfile

HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
  CMD wget -q -O /dev/null http://127.0.0.1:80/health || exit 1

O runner detecta o HEALTHCHECK da imagem e nao injeta nada. O wget do busybox do Alpine faz o check.

Backend Flask (Build mode)

project: meu-api
port: 8000
build:
  context: .
  dockerfile: Dockerfile

healthcheck:
  path: /health
  start_period: 45s
  interval: 10s
  retries: 5

Se o Dockerfile nao declara HEALTHCHECK, o runner injeta:

bash -c 'echo -e "GET /health HTTP/1.0\r\n..." > /dev/tcp/127.0.0.1/8000'

Se a imagem base e python:3.12-slim (Debian), bash esta disponivel. Se for python:3.12-alpine, bash nao existe — declare HEALTHCHECK no Dockerfile.

Servico SMTP (TCP)

project: meu-mta
port: 25

healthcheck:
  tcp: 25
  start_period: 60s
  interval: 30s
  retries: 5

O runner injeta nc -z 127.0.0.1 25 — nao precisa de HTTP.

Worker sem porta (sem healthcheck)

project: meu-worker
port: 0

healthcheck:
  path: ""

Sem healthcheck. O runner aceita running como sucesso. Se o container morrer (exited), sera detectado no proximo runner fetch --deploy.

Validacao

O runner validate e o runner add verificam a consistencia da config:

Check Erro
port: 0 com path: definido port must be a positive integer
path: health (sem /) must start with /
tcp: 0 must be a positive port number
cmd: [] (lista vazia) must be a non-empty list
# Validar antes de registrar
runner validate /data/apps/meu-app

Diagnostico

# Ver qual health check o runner vai usar
runner deploy meu-app -V

# Ver status de health do container rodando
docker inspect CONTAINER --format='{{.State.Health.Status}}'

# Ver logs do health check do Docker
docker inspect CONTAINER --format='{{json .State.Health.Log}}' | jq '.[].Output'

# Ver logs do container (quando health falha)
docker logs CONTAINER --tail 50

# Testar o endpoint manualmente de dentro do container
docker exec CONTAINER wget -q -O - http://127.0.0.1:PORT/health
docker exec CONTAINER curl -sf http://127.0.0.1:PORT/health

Migracoes Comuns

De imagem com curl para Alpine sem curl

Antes (funciona com Debian, quebra em Alpine):

healthcheck:
  path: /health

Depois (uma das tres opcoes):

# Opcao 1: TCP (mais simples, nao requer binarios)
healthcheck:
  tcp: 8000

# Opcao 2: CMD com wget do busybox do Alpine
healthcheck:
  cmd: ["wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8000/health"]

# Opcao 3: HEALTHCHECK no Dockerfile (recomendado)
# Adicione ao Dockerfile e remova healthcheck do .deploy.yml

De timeout curto para container lento

Antes (defaults — timeout total 105s):

healthcheck:
  path: /health

Depois (timeout total 255s):

healthcheck:
  path: /health
  start_period: 60s   # Grace antes de comecar
  interval: 15s        # Mais espaco entre checks
  timeout: 10s
  retries: 5           # Mais tentativas
By Borlot.com.br on 14/04/2026