Debugging a pod that will not start
A diagnostic path for the states you will actually meet — Pending, CrashLoopBackOff, ImagePullBackOff, OOMKilled — and what each one is really telling you.
Kubernetes tells you what is wrong. The information is just spread across four commands.
The order to run things
kubectl get pods # 1. what state is it in?
kubectl describe pod <name> # 2. read the Events at the bottom
kubectl logs <name> # 3. what did the app say?
kubectl logs <name> --previous # 4. what did it say before the restart?
Ninety percent of problems are answered by step 2. The Events list at the bottom of describe is
the single most useful output in Kubernetes and it is below the fold, so people miss it.
Pending — never scheduled
The scheduler could not place the pod anywhere.
kubectl describe pod <name> | tail -20
Events:
Warning FailedScheduling 0/3 nodes are available:
3 Insufficient cpu.
The message names the reason directly. Common ones:
| Message | Meaning |
|---|---|
Insufficient cpu / memory |
No node has room for your requests |
node(s) had untolerated taint |
Nodes are tainted and your pod has no toleration |
pod has unbound immediate PersistentVolumeClaims |
Storage could not be provisioned |
didn't match Pod's node affinity |
Your affinity rules exclude every node |
kubectl describe nodes | grep -A5 "Allocated resources"
That shows what is actually reserved. Note that it reflects requests, not usage — a node can be “full” while idle.
ImagePullBackOff / ErrImagePull
Failed to pull image "myapp:1.0": not found
Four causes, in order of likelihood:
- Typo in the image name or tag. Check it character by character.
- Private registry with no credentials. You need an image pull secret:
kubectl create secret docker-registry regcred \ --docker-server=registry.example.com \ --docker-username=user --docker-password=passspec: imagePullSecrets: - name: regcred - Wrong architecture. An
arm64image built on an Apple Silicon Mac will not run onamd64nodes. Build multi-arch withdocker buildx. - Rate limiting. Anonymous Docker Hub pulls are limited; authenticate or use a mirror.
CrashLoopBackOff — starts, then dies, repeatedly
The container is starting and exiting. Kubernetes restarts it with increasing backoff.
kubectl logs <name> --previous
Then check the exit code:
kubectl describe pod <name> | grep -A3 "Last State"
Last State: Terminated
Reason: Error
Exit Code: 1
| Exit code | Meaning |
|---|---|
| 0 | Completed normally — your command finishes instead of staying up |
| 1 | Application error — read the logs |
| 137 | SIGKILL — almost always OOMKilled |
| 139 | Segfault |
| 143 | SIGTERM — terminated normally during shutdown |
To get in and look around without the process crashing, override the command:
kubectl run debug --rm -it --image=myapp:1.0 --restart=Never -- sh
OOMKilled
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
The container exceeded its memory limit. Either the limit is too low or the application is leaking.
kubectl top pod <name> --containers
Raise the limit if the number is legitimate. If memory climbs steadily and never falls, you have a leak and raising the limit only delays the restart.
Running but not Ready
NAME READY STATUS RESTARTS
web-7d4b9c8f5-x2k9p 0/1 Running 0
The process is up, the readiness probe is failing, and the Service is correctly refusing to send it traffic.
kubectl describe pod <name> | grep -A5 "Readiness"
Usual causes: the probe path is wrong, the port is wrong, or initialDelaySeconds is too short
for the application’s startup. For slow-starting applications use a startupProbe rather than
inflating the initial delay on every probe.
A Service that does not work
kubectl get endpoints <service-name>
Empty means the selector matches no pods. Compare:
kubectl get pods --show-labels
kubectl describe svc <service-name> | grep Selector
If endpoints exist but connections still fail, test from inside:
kubectl run tmp --rm -it --image=nicolaka/netshoot --restart=Never -- bash
# then, in the shell:
nslookup my-service
curl -v http://my-service
nc -zv my-service 80
If DNS resolves but the connection is refused, the application is bound to 127.0.0.1 inside its
container instead of 0.0.0.0 — the same mistake as in the Docker guide, seen from the cluster.
Ingress returns 404 or 503
kubectl get ingress
kubectl describe ingress <name>
kubectl logs -n ingress-nginx deployment/ingress-nginx-controller
Check in this order:
- Is a controller installed and running? An Ingress with no controller does nothing.
- Is
ingressClassNameset and correct? - Does the backend Service exist, with that exact port name or number?
- Does the Service have endpoints? (the check above)
Cluster-wide view
kubectl get events -A --sort-by=.metadata.creationTimestamp | tail -30
The recent events across the whole cluster, newest last. When something is wrong and you do not know where, start here.
kubectl get pods -A | grep -v Running | grep -v Completed
Everything that is not healthy, in one line.
The mental checklist
kubectl get pods— which state?kubectl describe pod— read the Events at the bottomkubectl logs --previous— if it is restartingkubectl get endpoints— if it is a connectivity problemkubectl get events -A— if you do not know where to look
You have finished the guide
You understand the reconciliation loop, the objects that matter, and how to deploy, expose, update and debug a real application. That covers what most people need day to day.
What comes next is platform work — an ingress controller, cert-manager, monitoring, GitOps with Argo CD or Flux, and RBAC. Each is its own topic, and each is much easier now that the core model is in place.
If you have not read it yet, the Docker guide covers building the images this all runs, and the Terraform guide covers declaring the cluster itself.