Healthchecks: o contentor de pé que não responde
O docker ps diz Up 4 hours e o site não responde. As duas coisas não se contradizem: «Up» só quer dizer que o processo número um ainda não morreu. Se ele estiver encravado à espera de uma ligação que nunca chega, continua vivo, e o Docker continua a dizer que está tudo bem.
É para esse vão que existe o healthcheck: um comando que o Docker corre dentro do contentor, de tempos a tempos, para perguntar se ele ainda serve para alguma coisa.
1. A pergunta difícil não é «como», é «o quê»
Há duas perguntas diferentes, e misturá-las é a origem de quase todos os problemas:
Está vivo? — o processo responde, ou encravou. Se a resposta for não, a solução é reiniciar.
Está pronto? — consegue atender pedidos agora. Se a resposta for não, a solução é não lhe mandar tráfego — e não reiniciá-lo.
São respostas com consequências opostas. O HEALTHCHECK do Dockerfile, na prática, acaba quase sempre ligado a reinícios — e é por isso que a pergunta certa a pôr lá é a primeira.
2. O erro que custa caro
É pôr a base de dados no healthcheck. Parece rigor — «só está saudável se conseguir consultar a base» — e é uma armadilha.
Quando a base tem um soluço de trinta segundos, o check falha, o contentor é declarado doente e reiniciado. Só que reiniciar a aplicação não conserta a base. O que se consegue é transformar uma indisponibilidade momentânea num ciclo de reinícios — e, pelo caminho, deitar fora os pedidos que estavam a meio e qualquer estado quente que existisse.
O /healthz deste site é deliberadamente burro por causa disso:
export const dynamic = 'force-dynamic';
export function GET() {
return Response.json({ ok: true, uptime: process.uptime() });
}Não toca em nada. Diz só que há um processo de pé a servir HTTP — que é exatamente a pergunta a que um reinício sabe responder.
3. Os parâmetros, e a conta que eles fazem
HEALTHCHECK --interval=30s --timeout=3s --start-period=25s --retries=3 \
CMD wget -qO- http://127.0.0.1:3000/healthz >/dev/null || exit 1--start-period— o tempo de graça no arranque. Sem ele, um arranque de vinte segundos é declarado doente antes de chegar a responder. Já lhe tinha chamado nomes no artigo do contentor que não arranca.--timeout— o que mais produz falsos alarmes. Três segundos parecem muitos até a máquina estar com carga.--intervale--retries— multiplicam-se: com 30 s e 3 tentativas, passam um minuto e meio entre a aplicação encravar e alguém dar por isso.
4. O Docker não faz nada com o resultado
Esta apanha toda a gente uma vez. Um contentor marcado unhealthy continua a correr. O Docker regista o estado e mais nada — não reinicia, não avisa, não tira de serviço.
docker inspect --format '{{.State.Health.Status}}' o-meu-contentor
# unhealthy — e continua exatamente onde estavaQuem reage ao estado é outra coisa: um orquestrador, uma política de reinício, ou o próprio compose a segurar o arranque de um serviço até o outro estar pronto.
depends_on:
base:
condition: service_healthyAqui, sim, o healthcheck da base faz todo o sentido — porque quem o consome é quem decide esperar, não quem decide reiniciar.
5. O comando tem de existir na imagem
Um pormenor que dá horas perdidas: o curl não vem nas imagens pequenas. Numa Alpine há wget, que vem do BusyBox e tem outras opções. Um healthcheck que chama um comando inexistente falha sempre — e falha a dizer que a aplicação está doente, quando o doente é o check.
O resumo
Antes de escrever a linha, decidir qual das duas perguntas se está a fazer. Se a resposta a uma falha for «reiniciar», o check tem de ser barato e superficial — nada de bases de dados, nada de serviços de terceiros. As dependências pertencem ao outro check, o que serve para esperar e para encaminhar, nunca para matar.