C'est le message d'erreur le plus vu de tous ceux qui opèrent Kubernetes.
Et paradoxalement, l'un des plus mal diagnostiqués.
Parce que CrashLoopBackOff n'est pas une erreur.
C'est un symptôme : votre conteneur démarre, plante, et le kubelet réessaie avec un backoff exponentiel (10 s, 20 s, 40 s, jusqu'à 5 minutes maximum).
Kubernetes ne vous dit pas pourquoi ça crashe. Il vous dit juste que ça crashe de façon répétée.
La bonne nouvelle : dans 90 % des cas, la cause racine se trouve en moins de 15 minutes si on suit une méthode.
Étape 1 - Regarder le statut du pod
kubectl get pods -n mon-namespaceNAME READY STATUS RESTARTS AGE
api-7d9f8b6c5-x2vkm 0/1 CrashLoopBackOff 14 (2m ago) 32mDeux informations importantes ici :
- RESTARTS : combien de fois le conteneur a redémarré. Un compteur qui grimpe vite = crash systématique au démarrage. Un compteur qui monte lentement = crash après plusieurs minutes de fonctionnement (souvent une probe ou une fuite mémoire).
- AGE vs RESTARTS : si le pod a 32 minutes et 14 restarts, il ne survit jamais longtemps.
Étape 2 - Le describe, votre meilleure source
kubectl describe pod api-7d9f8b6c5-x2vkm -n mon-namespaceDeux sections à lire dans l'ordre :
Last State
Last State: Terminated
Reason: Error
Exit Code: 1C'est l'information la plus précieuse.
L'exit code vous dit qui a tué le processus et pourquoi (voir tableau plus bas).
Events
Events:
Type Reason Age Message
---- ------ ---- ----
Normal Scheduled 34m Successfully assigned
Normal Pulled 33m Container image pulled
Warning BackOff 2m (x98 over 31m) Back-off restarting failed containerLisez les événements du début à la fin, pas seulement les derniers.
Un FailedMount, un Unhealthy - Liveness probe failed ou un OOMKilled apparaissent souvent bien avant le premier BackOff.
Étape 3 - Les logs du crash précédent
L'erreur classique : faire un kubectl logs simple, qui montre les logs du conteneur actuel, donc souvent presque vides puisque le conteneur vient de redémarrer.
Le flag magique :
kubectl logs api-7d9f8b6c5-x2vkm --previous -n mon-namespace--previous récupère les logs du conteneur tel qu'il était au moment du crash. C'est là que votre application a écrit sa dernière volonté : stack trace Java, erreur Python, message de config manquante...
Dans la majorité des cas, la cause racine est écrite noir sur blanc dans ces logs.
Étape 4 - Interpréter l'exit code
| Exit Code | Signification | Cause probable |
|---|---|---|
| 0 | Sortie propre | Le process se termine volontairement - souvent un script qui finit trop vite (job, commande one-shot) |
| 1 | Erreur applicative | Exception non gérée, config manquante, connexion refusée au démarrage |
| 125 | Erreur du runtime | Problème containerd/docker - argument invalide, image cassée |
| 126 | Commande non exécutable | Binaire absent ou sans droits d'exécution |
| 127 | Commande introuvable | Typo dans la command: ou dépendance absente de l'image |
| 137 | SIGKILL (OOMKilled) | Le process a dépassé sa limite mémoire, ou a été tué manuellement |
| 139 | Segfault | Bug bas niveau, incompatibilité libc/architecture (ARM vs AMD64) |
| 143 | SIGTERM | Terminaison propre demandée - normal pendant un rollout |
Retenez surtout deux codes : 1 (l'application plante elle-même) et 137 (quelqu'un ou quelque chose l'a tuée).
Les 6 causes classiques, et leur fix
1. La liveness probe trop agressive
Symptôme : exit code 137 ou 143, le pod tourne quelques secondes puis meurt, les events montrent Liveness probe failed.
Le piège : la liveness probe tue le conteneur avant même que l'application ait fini de démarrer. Une app Java qui met 60 s à booter avec une probe toutes les 5 s et un failureThreshold: 3 n'a tout simplement jamais de chance.
Fix : utilisez une startupProbe qui protège la phase de démarrage, et gardez la liveness pour détecter les blocages en cours de vie :
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 5 # jusqu'à 150s pour démarrer
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 15
failureThreshold: 3Règle simple : une liveness probe ne doit jamais vérifier des dépendances externes (base de données, API tierce). Sinon, une panne de la dépendance transforme chaque pod en boucle de crash et vous avez multiplié le problème au lieu de le subir une fois.
2. L'OOMKilled (exit 137)
Symptôme : Last State: Terminated, Reason: OOMKilled.
Le conteneur dépasse sa limite mémoire (resources.limits.memory) et le kernel le tue.
Deux sous-cas très différents :
- Limite trop basse : l'app a besoin de 800 Mo, on lui a mis
512Mi.
Augmentez la limite, mais cherchez aussi pourquoi la consommation a changé. - Vraie fuite mémoire : la consommation grimpe progressivement jusqu'à la limite.
Le pod survit alors de plus en plus longtemps entre deux crashes - c'est le pattern « RESTARTS qui monte lentement » de l'étape 1.
Attention aux runtimes gourmands : une JVM sans -XX:MaxRAMPercentage calibré, ou un V8 Node.js, dimensionnent leur heap indépendamment des limites cgroup si on ne les configure pas explicitement.
3. La configuration manquante
Symptôme : exit code 1 dès la première seconde, et dans les logs précédents un message limpide du type :
Error: Environment variable DATABASE_URL is requiredÇa arrive typiquement après un renommage de Secret ou ConfigMap, un déploiement dans un nouveau namespace, ou un GitOps qui a syncé un manifeste incomplet.
Fix court terme : recréer la ressource manquante.
Fix long terme : validez vos manifests avant déploiement (kubeconform, policy engine type Kyverno) et faites du secret externe (External Secrets Operator, Vault) plutôt que des copier-coller entre namespaces.
4. La dépendance qui n'est pas prête
Symptôme : exit code 1, logs montrant ECONNREFUSED vers la base de données, et ça finit par passer... parfois.
L'application démarre plus vite que sa base de données. Le pattern sain n'est pas de mettre un sleep 30 dans l'entrypoint, mais de rendre le démarrage résilient : retry avec backoff côté application, ou initContainer qui attend la disponibilité :
initContainers:
- name: wait-for-db
image: postgres:17
command: ["sh", "-c", "until pg_isready -h $DB_HOST; do sleep 2; done"]5. Les permissions et le filesystem
Symptôme : exit code 126 ou 1, erreurs Permission denied ou Read-only file system dans les logs.
Fréquent quand on durcit la sécurité : readOnlyRootFilesystem: true dans le securityContext, un utilisateur non-root (runAsUser: 1000) et une application qui veut écrire dans /tmp ou /var/log.
Fix : montez un emptyDir sur les chemins d'écriture nécessaires :
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}6. Le vrai crash applicatif
Et parfois, Kubernetes fonctionne parfaitement : c'est le code qui plante. Stack trace claire dans les logs précédents, reproductible localement, lié à une release récente.
Dans ce cas, la question utile à se poser est : qu'est-ce qui a changé ? Nouvelle image, nouvelle version de dépendance, migration de schéma ? Le kubectl rollout undo deployment api reste votre meilleur ami pour reprendre la production pendant qu'on corrige.
Quand le problème n'est pas l'application
Si les logs précédents sont vides ou incohérents, sortez du pod et regardez autour :
- DNS : l'app plante car elle ne résout aucun nom, testez avec un pod utilitaire (
nslookup kubernetes.default) - CNI / réseau : connexions refusées aléatoires selon les nœuds, suspectez un problème de plugin réseau après un upgrade
- Pression nœud :
kubectl describe nodemontre des évictions ou du memory pressure, vos pods sont victimes, pas coupables
La checklist récapitulative
kubectl get pods→ vitesse de montée des RESTARTSkubectl describe pod→ Last State + Exit Code + Events depuis le débutkubectl logs --previous→ la cause est écrite là dans 90 % des cas- Exit 137 ? → OOMKilled ou probe qui tue
- Exit 1 au boot ? → config manquante ou dépendance pas prête
- Logs vides ? → soupçonnez DNS, CNI ou le node
- Rien de logique ? → qu'est-ce qui a changé récemment ?
Ce genre de boucle de crash qui résiste à la checklist, ça arrive. Quand le diagnostic traîne et que la production chauffe, un regard extérieur débloque souvent en quelques heures ce qui bloque depuis plusieurs jours.