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: 30sO 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
HEALTHCHECKno 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: 3O 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: 3O 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 1Nesse caso o runner:
- Faz
docker inspect --format '{{json .Config}}' <imagem> - Encontra
Healthcheck.Test: ["CMD-SHELL", "wget ..."] - Loga:
Honoring HEALTHCHECK declared by image - NAO passa
--health-cmdao docker run
Beneficios:
- Voce testa o healthcheck localmente (
docker build && docker run) - Nao depende de
curl/wget/bashestar 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 = 105sstart_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: 5Tolerancia 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: 3Pra php-fpm:8.2 EXPOSE 9000:
healthcheck:
mode: tcp
tcp: 9000
interval: 30s
...Sempre dá pra override: o user pode editar o
.deploy.ymlgerado 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: exitComportamento:
| 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 --rmsob 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: 3O 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 1O 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: 5Se 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: 5O 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-appDiagnostico
# 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/healthMigracoes Comuns
De imagem com curl para Alpine sem curl
Antes (funciona com Debian, quebra em Alpine):
healthcheck:
path: /healthDepois (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.ymlDe timeout curto para container lento
Antes (defaults — timeout total 105s):
healthcheck:
path: /healthDepois (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