Index du journalDockup / note de terrain
Note / health-check-failing-deployment

Le health check échoue, mais l’application fonctionne

Lorsqu’un health check échoue au déploiement alors que l’application fonctionne parfaitement en local, cinq causes reviennent généralement. Vérifiez le binding, le chemin, le port, le délai et les dépendances dans l’ordre qui permet d’identifier le problème le plus rapidement.

Il existe une forme bien particulière de blocage où un health check qui échoue au déploiement empêche toute mise en production alors que l’application est, selon tous les indicateurs accessibles, parfaitement opérationnelle. Elle fonctionne en local. Elle fonctionne dans Docker en local. Les logs indiquent qu’elle est en écoute. Et la plateforme signale échec sur échec, parfois vingt fois de suite, sans qu’aucune requête n’apparaisse dans votre access log.

Ce dernier détail est essentiel et réduit immédiatement le champ des possibilités. Si votre application n’a jamais enregistré la requête, c’est que le check n’a jamais atteint votre application — rien dans le code de l’application ne pourra donc l’expliquer.

Voici les cinq causes, dans l’ordre qui permet de trouver le problème le plus rapidement.

1. Vous êtes bindé sur localhost

C’est de loin la cause la plus fréquente, et elle explique exactement le symptôme « aucun trafic n’arrive ».

Dans un conteneur, 127.0.0.1 désigne la boucle locale de ce conteneur. Un health check provenant de l’extérieur du conteneur ne peut pas l’atteindre. Le processus est en écoute, vos logs le confirment, mais le socket est inaccessible depuis l’endroit qui compte.

// Unreachable from outside the container
app.listen(3000, '127.0.0.1')

// Correct
app.listen(3000, '0.0.0.0')

Les frameworks n’ont pas tous les mêmes valeurs par défaut, et plusieurs d’entre eux les ont modifiées entre deux versions majeures. Vérifiez ce sur quoi votre framework se bind réellement, plutôt que de vous fier à votre souvenir.

# Confirm from inside the running container
dockup exec "ss -ltn || netstat -ltn" my-project/my-api

Si l’adresse en écoute est 127.0.0.1:3000 au lieu de 0.0.0.0:3000, vous avez trouvé la cause et le reste de cette liste n’a plus d’importance.

2. Le port sondé par la plateforme n’est pas celui sur lequel vous servez l’application

Deux ports sont concernés et il est facile de les confondre : le port sur lequel votre processus écoute à l’intérieur du conteneur, et le port vers lequel la plateforme route le trafic. Si votre application lit PORT dans l’environnement alors que vous avez écrit 3000 en dur quelque part dans un Dockerfile, ces deux valeurs peuvent diverger silencieusement.

La méthode fiable consiste à laisser la plateforme vous indiquer le port :

const port = process.env.PORT || 3000
app.listen(port, '0.0.0.0')

Définissez ensuite le port du service une seule fois, dans la plateforme, et cessez de maintenir ce nombre à deux endroits.

3. Le chemin renvoie autre chose qu’un succès

Un chemin de health check est comparé exactement, et un nombre étonnamment élevé d’échecs sont en réalité des redirections. Si votre application redirige /healthz vers /healthz/, ou force HTTPS avec une 301, un checker qui ne considère que les réponses 2xx comme des succès échouera à chaque fois, tandis qu’un navigateur suivra la redirection et vous affichera une page fonctionnelle.

Trois pièges précis :

  • Redirections liées au slash final. /healthz/healthz/ est une 301.
  • HTTPS forcé. Le check interne arrive généralement en HTTP simple sur la boucle locale. Une redirection HTTPS inconditionnelle le fait échouer.
  • Middleware d’authentification. Un guard d’authentification global exécuté avant le routage renverra également une 401 pour le chemin de health check.

Excluez explicitement le chemin de health check de l’authentification et de l’application forcée de HTTPS. C’est la route qui doit être la plus simple possible.

4. Le check est plus rapide que votre cold start

Si le check échoue quelques fois puis réussit, ou s’il échoue au déploiement avant de fonctionner lorsque vous le relancez, il s’agit d’un problème de timing, pas de configuration.

Le budget dont vous avez besoin ne correspond pas à une seule tentative : c’est intervalle × nombre de tentatives. Une application qui met douze secondes à se connecter à sa base de données et à préchauffer un cache a besoin d’un budget total supérieur à douze secondes. Sinon, chaque mise en production échouera et vous finirez par désactiver le gate, supprimant ainsi la seule protection entre un build défaillant et vos utilisateurs.

dockup info my-project/my-api --json | grep -A6 healthCheck

Définissez un timeout supérieur à la durée de votre tentative légitime la plus lente, puis configurez suffisamment de tentatives pour que intervalle × nombre de tentatives dépasse largement la durée de votre démarrage légitime le plus lent. Mesurez le démarrage au lieu de le deviner : les logs contiennent les timestamps.

5. L’application n’est réellement pas prête

C’est le dernier cas, et la raison d’être du check : votre application a démarré, n’a pas réussi à joindre une dépendance et réessaie. Elle n’a pas planté, donc rien ne la redémarre. Elle ne peut pas traiter les requêtes, donc le check échoue. Le système fonctionne exactement comme prévu et vous indique que cette version ne doit pas recevoir de trafic.

Pour le distinguer des quatre autres cas, vérifiez que votre application a bien enregistré la requête et a répondu avec un statut différent de 2xx. Si la requête apparaît dans vos logs, les causes 1 à 3 sont écartées.

L’ordre de diagnostic qui fait gagner du temps

# 1. Did the request reach the app at all?
dockup logs my-project/my-api --follow

# 2. What is the process actually bound to?
dockup exec "ss -ltn || netstat -ltn" my-project/my-api

# 3. Does the path answer from inside the container?
dockup exec "curl -si localhost:3000/healthz" my-project/my-api

# 4. What is the gate configured to expect?
dockup info my-project/my-api --json

L’étape 3 permet de résoudre la majorité de ces problèmes. Un curl exécuté depuis l’intérieur du conteneur élimine d’un coup toutes les variables réseau : s’il renvoie 200 à cet endroit alors que la plateforme échoue toujours, le problème vient de l’adresse ou du port, pas de l’application. S’il renvoie une 301 ou une 401, vous avez identifié la cause sans avoir à modifier quoi que ce soit sur la plateforme.

Pourquoi il vaut la peine de conserver le gate

Après le quatrième déploiement échoué, il peut être tentant de désactiver le health check pour mettre la version en production. Il est utile de se rappeler ce que vous désactivez.

Sur Dockup, le health gate est ce qui empêche une version défaillante d’atteindre vos utilisateurs. La nouvelle version est buildée et démarrée tandis que la version actuelle continue de servir le trafic ; celui-ci ne bascule que lorsque la nouvelle version répond. Désactiver le gate réactive le scénario où un conteneur qui démarre mais ne peut pas fonctionner remplace une version opérationnelle.

Un check qui échoue quatre déploiements de suite est agaçant. Un check qui réussit systématiquement est un check qui n’arrêtera pas le déploiement qui compte vraiment.

Foire aux questions

Pourquoi le health check échoue-t-il alors que l’application fonctionne en local ? Presque toujours parce que le conteneur se bind sur 127.0.0.1 au lieu de 0.0.0.0. En local, vous vous connectez via la même boucle locale ; depuis l’extérieur du conteneur, cette adresse est inaccessible.

Le endpoint de health check doit-il nécessiter une authentification ? Non. Excluez-le du middleware d’authentification global, sinon le checker recevra une 401 et le déploiement échouera alors que l’application fonctionne.

Quel timeout dois-je utiliser ? Un timeout supérieur à la durée de votre tentative légitime la plus lente, avec suffisamment de tentatives pour couvrir votre cold start légitime le plus lent. Consultez la durée du démarrage dans vos logs au lieu de la deviner.

Est-il prudent de désactiver le health check pour débloquer un déploiement ? Cela débloque le déploiement, mais supprime la protection qui empêche une version défaillante de recevoir du trafic. Corrigez plutôt le check : dans la plupart des cas, la cause est une adresse de binding ou une redirection, et le problème se règle en quelques minutes.