Healthcheck failed

Sintoma:

[FAIL] meu-app  Phase 1.4: failed health check after 30s

Ou no modo HTTP:

[FAIL] meu-app  /health returned 502 after 30s (3/3 retries)

Causa

O container do deploy novo subiu, mas a probe configurada no manifest (http/tcp/cmd) não retornou OK no prazo definido. O Runner é blue-green: ele mantém o container antigo servindo tráfego até o novo passar no health. Por isso, healthcheck failed = rollback automático, seu site continua no ar.

Diagnóstico

runner status meu-app             # container state atual (running/stopped)
runner logs tail meu-app          # logs do container que falhou
runner debug last-error meu-app   # último DeployFailure estruturado
runner health meu-app             # roda a probe manualmente uma vez

Olha primeiro o debug last-error — ele já vem com a mensagem de erro e fix sugerido.

v2.50.0+: o rollback destrói o container falho, mas antes o runner arquiva as últimas 200 linhas de docker logs em <logs_dir>/failures/<app>/<version>_<timestamp>.log (default /opt/runner/logs/failures/...). A mensagem de erro do deploy aponta o caminho exato. Não use docker logs no container que sobrou — é o da versão anterior, que voltou no rollback.

O último probe e a sugestão

O erro do deploy traz o resultado do último probe que reprovou, lido do container antes de ele ser removido:

  último probe: modo cmd · alvo `curl -f http://localhost:8080/health` · exit 127
  saída: /bin/sh: curl: not found
  sugestão: a imagem não tem `curl`; use `tcp: 8080`
  • modo e alvo: http com o path, tcp com a porta, cmd com o comando.
  • exit: o código do probe. 127 é comando não encontrado; -1 é o Docker que nem conseguiu rodar o probe (ex.: a imagem não tem sh).
  • saída: até 300 caracteres. Os segredos da app (secrets:, .secrets, cofre) e tokens com cara de token saem como ***.

Quando o probe registrado é o HEALTHCHECK da própria imagem (modos external_* e container_state, que não injetam sonda, ou quando o deploy honrou o HEALTHCHECK da imagem), a linha diz modo HEALTHCHECK da imagem, o alvo é o comando dele e não há sugestão: o manifesto não decide esse probe.

O mesmo vai no evento deploy_failure, no campo ultimo_probe (modo, alvo, exit, saida; modo: imagem para o HEALTHCHECK da imagem); o campo não aparece quando não houve probe. O last_error da app (runner status) continua começando por "health check did not pass", com o último probe e a sugestão depois.

A sugestão aparece quando a causa é reconhecível:

O probe disse Sugestão
exit 127, sh: curl: not found ou exec: "curl": executable file not found a imagem não tem curl; use tcp: <porta>
exec: "/bin/sh": … no such file or directory (imagem scratch/distroless), em qualquer modo (cmd, path: ou tcp:) a imagem não tem sh; use mode: external_tcp com tcp: <porta> (o tcp: também roda com sh; o external_tcp testa a porta de fora do container)
a sonda path: do runner sai 127 a imagem não tem wget, curl nem python; use tcp: <porta>
Connection refused com localhost no cmd, em imagem Alpine ou com ::1 na saída troque localhost por 127.0.0.1 (o wget do busybox resolve localhost para ::1)
resposta 401 ou 403 o path está atrás de autenticação; aponte o healthcheck para um path sem login, como /health

O runner manifest validate (sem --estatico) já acusa antes do deploy a imagem sem shell e o localhost em Alpine; o "não tem curl" só aparece na hora do probe.

Fix — cenário 1: timeout muito curto

Se a app demora 40s pra subir mas o healthcheck tem timeout 30s:

# .deploy.yml
healthcheck:
  mode: http
  path: /health
  timeout: 60s   # default 30s
  retries: 3

Commit + push na branch dist → próximo fetch --deploy aplica.

Fix — cenário 2: app demora pra bootar (migrations, cache warm)

Use start_period pra ignorar falhas durante o boot:

healthcheck:
  mode: http
  path: /health
  start_period: 60s   # ignora primeiros 60s

Fix — cenário 3: endpoint errado ou modo errado

Confirme que a app expõe /health:

docker exec -it $(docker ps -q --filter name=meu-app) curl localhost:8080/health

Se não expõe, mude pra mode: tcp (TCP connect) ou crie o endpoint. Veja Healthcheck modes.

Fix — cenário 4: app realmente não sobe (crash loop)

runner logs history meu-app   # ver logs do último container parado

Se for bug de código: corrige + push. Se for env var faltando: veja Secrets lifecycle.

Como prevenir

  • Teste localmente com docker build && docker run antes de push.
  • Configure start_period se a app tem migrations no boot.
  • Use mode: tcp se HTTP é overkill — a probe TCP é mais leve.
  • Configure notificações (Notificações) para ser avisado de fails sem precisar checar manualmente.

Conceito relacionado

Por que o site continuou no ar? Veja GitOps mode — o Runner não substitui o container até o health passar.

By Borlot.com.br on 27/05/2026