TL;DR
CrashLoopBackOff não é o erro: é o kubelet avisando que o container caiu, foi reiniciado, caiu de novo, e que agora ele espera cada vez mais entre tentativas: 10 s, 20 s, 40 s, até o teto de 5 minutos (documentação do Kubernetes, "Pod Lifecycle"). A causa está dentro do container. Cinco comandos resolvem a maioria dos casos: get pods para confirmar o padrão de reinícios, describe pod para ler o Last State e o Exit Code, logs --previous para ver o que o container escreveu antes de morrer, get events para o que o kubelet reportou e exec ou debug para testar a hipótese de dentro. O exit code aponta a família da causa: 137 é memória (OOMKilled), 1 é erro da aplicação, 127 é comando não encontrado.
O que o CrashLoopBackOff significa (e o que não significa)
Todo pod tem um restartPolicy, e o padrão é Always. Quando um container termina, com sucesso ou não, o kubelet o reinicia. Se ele terminar de novo logo depois, o kubelet passa a esperar antes de tentar: 10 segundos, depois 20, 40, 80, e assim por diante, com teto de 5 minutos. Se o container ficar 10 minutos rodando sem cair, o contador zera. Esses números vêm da documentação do Kubernetes, seção "Pod Lifecycle", item "Container restart policy".
Enquanto espera, o pod mostra CrashLoopBackOff na coluna STATUS. É um estado de espera, não uma causa. A pergunta certa não é "por que o Kubernetes está reiniciando meu pod?", e sim "por que meu processo termina?". As respostas moram em dois lugares: no exit code do último término e no log do container anterior.
notaDeletar o pod cria um pod novo, sem o back-off acumulado, e ganha alguns segundos de Running. Não muda o exit code seguinte. Se você já deletou três vezes hoje, pule para o comando 2.
comando 1Confirme o padrão com kubectl get pods
Antes de qualquer hipótese, olhe a coluna RESTARTS. Um pod com 6 reinícios em 9 minutos está em loop; um pod com 1 reinício em 3 dias teve um soluço. O kubectl mostra há quanto tempo foi o último reinício entre parênteses. Um detalhe que engana: um pod em CrashLoopBackOff continua na fase Running (quem está em espera é o container), então filtrar por status.phase não o encontra. Filtre pela coluna STATUS.
kubectl -n loja get pods
# só o que não está Running na coluna STATUS:
kubectl -n loja get pods --no-headers | grep -v ' Running ' NAME READY STATUS RESTARTS AGE
catalogo-5c4b7d8f9-p9m2r 1/1 Running 0 3d2h
checkout-7d9f8b6c4-x2k4q 0/1 CrashLoopBackOff 6 (42s ago) 9m17s
pagamentos-6f8d1c2a7-k3z7w 1/1 Running 0 3d2h comando 2Leia o Last State com kubectl describe pod
O describe é longo; o que importa está em dois blocos. Last State diz como o container anterior terminou: Reason e Exit Code. Events, no fim, mostra o que o kubelet fez e quantas vezes.
kubectl -n loja describe pod checkout-7d9f8b6c4-x2k4q
# só o estado anterior, sem o resto:
kubectl -n loja get pod checkout-7d9f8b6c4-x2k4q -o jsonpath='{.status.containerStatuses[0].lastState}' State: Waiting
Reason: CrashLoopBackOff
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Restart Count: 6
Limits:
cpu: 500m
memory: 512Mi
Requests:
cpu: 250m
memory: 256Mi
Events:
Type Reason Age From Message
Warning BackOff 38s (x26 over 8m51s) kubelet Back-off restarting failed container checkout Aqui o caso já está quase fechado: Reason OOMKilled, Exit Code 137, limite de 512Mi. O container passou do limite de memória e o kernel o matou. A tabela de exit codes mais abaixo cobre os outros valores.
comando 3O log que importa é o anterior: kubectl logs --previous
O erro mais comum de quem está começando: rodar kubectl logs e ver o container atual, que acabou de subir e ainda não deu erro. A flag --previous (ou -p) mostra o log do container que morreu.
kubectl -n loja logs checkout-7d9f8b6c4-x2k4q --previous --tail=100
# pod com mais de um container: nomeie o container
kubectl -n loja logs checkout-7d9f8b6c4-x2k4q -c checkout --previous --timestamps {"level":"info","msg":"iniciando checkout 2.14.0"}
{"level":"info","msg":"carregando catálogo em memória","itens":412000}
{"level":"warn","msg":"heap em 91% do limite do container"}
(fim do log: o processo foi morto sem escrever mais nada) Num OOMKilled o log costuma terminar no meio do trabalho: o kernel não avisa o processo. A última linha antes do silêncio é a pista. Num erro de aplicação (exit 1), ao contrário, o log geralmente termina com a exceção completa.
comando 4O que o cluster disse: kubectl get events
Os eventos contam a história em ordem: agendado, imagem puxada, container criado, iniciado, back-off. Se aparecer um Unhealthy antes do Killing, a causa é uma liveness probe, não a aplicação. Eventos são retidos por 1 hora por padrão (flag --event-ttl do kube-apiserver); depois disso, só no seu sistema de observabilidade.
kubectl -n loja get events --sort-by=.lastTimestamp \
--field-selector involvedObject.name=checkout-7d9f8b6c4-x2k4q LAST SEEN TYPE REASON MESSAGE
9m18s Normal Scheduled Successfully assigned loja/checkout-7d9f8b6c4-x2k4q to aks-nodepool1-vmss000002
9m17s Normal Pulled Container image "registry.loja.internal/checkout:2.14.0" already present on machine
41s Normal Created Created container checkout
41s Normal Started Started container checkout
12s Warning BackOff Back-off restarting failed container checkout comando 5Teste a hipótese de dentro: kubectl exec e kubectl debug
Se o container fica de pé por alguns segundos, exec dá tempo de conferir variáveis e conectividade. Se não fica, kubectl debug anexa um container efêmero ao pod, compartilhando o namespace de processos do container alvo (documentação do Kubernetes, "Debug Running Pods"; contêineres efêmeros são estáveis desde a versão 1.25).
# variáveis que o container realmente recebeu
kubectl -n loja exec checkout-7d9f8b6c4-x2k4q -- env | grep -iE 'db_|redis_|java_'
# o container não fica de pé: container efêmero com ferramentas
kubectl -n loja debug -it checkout-7d9f8b6c4-x2k4q --image=busybox:1.36 --target=checkout
# testar a dependência de dentro do pod
nc -zv -w 3 postgres.loja.svc.cluster.local 5432 Tabela: o exit code aponta a família da causa
Códigos acima de 128 seguem a convenção POSIX: 128 mais o número do sinal que matou o processo. Os demais são definidos pela própria aplicação ou pelo shell.
| Exit code | Origem | Família da causa | Onde olhar |
|---|---|---|---|
| 0 | processo terminou normalmente | O comando principal acaba. Comum em imagens cujo CMD roda um script e sai. | Dockerfile: CMD e ENTRYPOINT; o processo precisa ficar em primeiro plano |
| 1 | erro genérico da aplicação | Exceção não tratada, configuração inválida, dependência recusando conexão. | logs --previous: a exceção costuma estar nas últimas linhas |
| 126 | shell: não executável | O entrypoint existe mas não tem permissão de execução. | Dockerfile: chmod +x ou usuário errado |
| 127 | shell: command not found | Binário ausente na imagem ou caminho errado no CMD. | Dockerfile e imagem base (distroless, alpine) |
| 137 | 128 + 9 (SIGKILL) | Quase sempre OOMKilled: o container passou do limite de memória. | describe pod: Reason OOMKilled; limits.memory |
| 139 | 128 + 11 (SIGSEGV) | Falha de segmentação: binário nativo, biblioteca incompatível com a arquitetura. | imagem: arquitetura (amd64 ou arm64) e dependências nativas |
| 143 | 128 + 15 (SIGTERM) | Encerrado pelo kubelet: liveness probe falhou, rollout, eviction ou scale-down. | get events: Unhealthy e Killing antes do término |
fontes: convenção POSIX de códigos de saída; lista de sinais do Linux (man 7 signal); documentação do Kubernetes, "Pod Lifecycle"
As 5 causas mais comuns e a correção de cada uma
O limits.memory foi copiado de um tutorial ou de outro serviço. A aplicação carrega cache, catálogo ou pool na subida e passa do teto.
Correção: Meça o uso real e defina o limite acima do pico. Em JVM e Node, faça o runtime respeitar o limite do container (MaxRAMPercentage, --max-old-space-size). Veja a página de erro OOMKilled.
O banco, a fila ou o gerenciador de segredos ainda não respondiam quando o processo iniciou, e a aplicação encerra em vez de tentar de novo.
Correção: Retry com back-off dentro da aplicação e uma startupProbe com failureThreshold generoso. initContainers que esperam para sempre só escondem o problema.
A chave existe (senão seria CreateContainerConfigError), mas o valor está vazio, com quebra de linha no fim ou em base64 duplo.
Correção: kubectl exec -- env para ver o que o container recebeu; kubectl get secret -o jsonpath e decodifique o valor.
O CMD roda uma migração ou um script e sai com sucesso. Para o Kubernetes, um container que termina precisa ser reiniciado.
Correção: Processo em primeiro plano no CMD. Tarefa que termina é Job, não Deployment.
A aplicação demora a ficar pronta e a liveness probe usa os padrões: período 10 s, timeout 1 s, 3 falhas. O kubelet mata um processo saudável.
Correção: startupProbe para a subida e liveness com timeout e failureThreshold realistas (documentação do Kubernetes, "Configure Liveness, Readiness and Startup Probes").
resources:
requests: { cpu: 250m, memory: 512Mi }
limits: { cpu: 500m, memory: 1Gi }
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75.0" Como o Kubepier mostra isso
O Kubepier mostra os eventos do pod, com os avisos primeiro, e o log. O diagnóstico por IA explica o erro a partir deles, em português: causa provável, evidência e correção sugerida. Você usa a sua própria chave de API (Anthropic ou OpenAI). Abaixo, uma ilustração do painel para o pod deste guia, com dados fictícios.
exit 137
request 256Mi
O container excede o limite de memória ao carregar o catálogo na subida. Evidência: OOMKilled com exit 137 nos eventos e aviso de heap a 91% antes do término, no log. Correção sugerida: subir limits.memory acima do uso real e alinhar o heap da JVM ao limite (MaxRAMPercentage).
Perguntas frequentes
CrashLoopBackOff é um erro do Kubernetes ou da minha aplicação?
Da aplicação, quase sempre. O Kubernetes só reporta que o container terminou e que está esperando cada vez mais entre reinícios. A causa está no exit code (describe pod) e no log anterior (logs --previous). As exceções são a liveness probe mal configurada e o limite de memória baixo, que são configuração sua no manifesto.
Quanto tempo o kubelet espera entre reinícios?
Começa em 10 segundos e dobra a cada falha (20, 40, 80…), com teto de 5 minutos. Depois que o container roda 10 minutos sem cair, o contador zera. Fonte: documentação do Kubernetes, "Pod Lifecycle", item "Container restart policy".
Por que kubectl logs não mostra nada?
Sem --previous você vê o container atual, que acabou de subir. Use --previous (ou -p) para o container que morreu. Se ainda assim estiver vazio, o processo morreu antes de escrever: típico de OOMKilled, de exit 127 (binário não encontrado) e de aplicações que só escrevem log depois de conectar ao banco.
Deletar o pod resolve?
O pod novo começa sem o back-off acumulado, então parece resolver por alguns segundos. Se a causa não mudou, o exit code volta. Vale a pena só quando a dependência externa já foi corrigida e você não quer esperar a próxima tentativa.
Exit code 137 é sempre OOMKilled?
Não. 137 é 128 + 9, ou seja, SIGKILL. O OOM killer do kernel usa SIGKILL, mas o kubelet também usa SIGKILL quando o container não encerra dentro do terminationGracePeriodSeconds (30 s por padrão). Confirme pelo Reason no describe pod: OOMKilled aparece explicitamente.
Como evitar o CrashLoopBackOff em deploys?
Requests e limits medidos, startupProbe para aplicações lentas na subida, retry com back-off para dependências e rollout com maxUnavailable 0. Acompanhe o primeiro pod novo antes de deixar o rollout continuar: kubectl rollout status mostra se ele ficou Ready.