Init:CrashLoopBackOff means an init container is failing. Init containers run in order, before any app container, and each must exit 0 before the next one starts. One failing init container means the main container never runs at all.
The status tells you where you are:
| Status | Meaning |
|---|---|
Init:0/2 | First of two init containers still running |
Init:1/2 | First succeeded, second running |
Init:Error | An init container exited non-zero |
Init:CrashLoopBackOff | It has failed repeatedly and is being backed off |
Read the right logs
This is where most time gets lost:
kubectl logs my-pod
Error from server (BadRequest): container "app" in pod "api-..." is waiting to start: PodInitializing
That is the main container, which has never started. Name the init container:
kubectl describe pod my-pod | grep -A3 "Init Containers:"
kubectl logs my-pod -c init-db
kubectl logs my-pod -c init-db --previous # if it is backing off
--previous matters here. In CrashLoopBackOff the container is often not running when you look, so the current logs are empty and the useful output is in the last terminated instance.
Pattern 1: Waiting for a dependency that will never arrive
The most common init container in the wild:
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z postgres 5432; do sleep 2; done']
If postgres is not resolvable, this loops forever and the Pod sits in Init:0/1 rather than crashing, which is arguably worse because there is no error to read.
kubectl get svc postgres
kubectl get endpoints postgres
An empty ENDPOINTS means the Service exists and matches no ready Pods, so the name resolves and nothing answers. That is a different fix from the Service not existing at all.
Give the wait a bound so it fails visibly instead of hanging:
command:
- sh
- -c
- |
for i in $(seq 1 60); do
nc -z postgres 5432 && exit 0
echo "waiting for postgres ($i/60)"
sleep 2
done
echo "postgres did not become reachable" >&2
exit 1
Two minutes then a clear failure, with progress in the logs throughout.
Worth asking whether the wait is needed at all. An application that retries its own database connection with backoff handles a restarting database mid-life too, which an init container cannot. The init container only covers the first few seconds of the Pod's existence.
Pattern 2: The migration that cannot be run twice
initContainers:
- name: migrate
image: myapp:1.4.2
command: ['./migrate', 'up']
Init containers re-run on every Pod restart, and with multiple replicas they run concurrently, one per Pod. A migration tool without locking then has several processes racing on the same schema, and all but one fail.
Most tools handle this: Flyway, Liquibase and Rails all take a lock. If yours does not, a Job run once before the rollout is the correct shape, not an init container.
The same applies to anything non-idempotent. mkdir /data/foo fails the second time; mkdir -p /data/foo does not.
Pattern 3: Permissions on a volume
initContainers:
- name: fix-perms
image: busybox:1.36
command: ['sh', '-c', 'chown -R 1000:1000 /data']
volumeMounts:
- name: data
mountPath: /data
securityContext:
runAsUser: 0
Without runAsUser: 0 the chown fails with Operation not permitted, and a securityContext at Pod level applies to init containers too. A cluster enforcing a restricted Pod Security Standard blocks runAsUser: 0 entirely, so the Pod will not even be admitted.
The better answer where the storage supports it is fsGroup, which lets the kubelet set ownership without a privileged container:
spec:
securityContext:
fsGroup: 1000
This works for most CSI drivers and removes the init container completely.
Resources apply differently
The scheduler uses the maximum of any single init container's request and the sum of the app containers' requests, not the total of everything. An init container with a large memory request can therefore make a Pod unschedulable in a way that is not obvious from reading the manifest.
Limits still apply individually, so an init container that exceeds its own memory limit is OOMKilled and you see Init:Error with exit code 137.
kubectl get pod my-pod -o jsonpath='{.status.initContainerStatuses[0].lastState.terminated}' | jq
restartPolicy governs init containers too
With restartPolicy: Always, a failed init container is retried with exponential backoff up to five minutes, hence CrashLoopBackOff. With restartPolicy: Never, which is common for Jobs, the whole Pod goes to Failed on the first init failure and stops. That is why the same broken init container loops forever in a Deployment and fails once in a Job.
A checklist
kubectl describe podand get the init container's name.kubectl logs <pod> -c <init-container>, adding--previousif it is backing off.Init:0/Nand no crash → it is hanging, usually waiting on a dependency.kubectl get endpoints <service>. Empty means nothing ready is behind it.- Bound every wait loop with a timeout and a non-zero exit.
- Migrations → use a Job, or confirm your tool locks.
Operation not permittedon a chown →runAsUser: 0, or better,fsGroup.- Exit code 137 → the init container hit its own memory limit.
Frequently Asked Questions
Why does kubectl logs not show my init container's output?
Because kubectl logs defaults to the first app container, which in this situation has never started, so it returns a PodInitializing message instead of anything useful. Pass -c <init-container-name> to select the init container explicitly; the name is in kubectl describe pod under Init Containers. If the Pod is in CrashLoopBackOff the container may not be running at the moment you look, so add --previous to read the last terminated instance, which is the one that actually failed.
What is the difference between Init:Error and Init:CrashLoopBackOff?
Init:Error means the init container exited non-zero, and Init:CrashLoopBackOff means it has done so repeatedly and the kubelet is now delaying each retry with exponential backoff, up to five minutes. They are the same underlying failure at different stages. Which one you end up in depends on the Pod's restartPolicy: Always retries and reaches CrashLoopBackOff, while Never, common in Jobs, marks the Pod Failed after the first failure and stops.
Do init containers run again when a Pod restarts?
Yes. Every init container runs again, in order, each time the Pod starts, including after a node reboot or an eviction and reschedule. That makes them unsuitable for anything not safely repeatable. It also means that with several replicas they run concurrently, one per Pod, so a database migration in an init container has multiple copies racing unless the migration tool takes a lock. A Job run once before the rollout is the right shape for genuinely one-time work.
Why is my Pod stuck at Init:0/1 with no error?
The init container is running and not exiting, which usually means a wait loop whose condition never becomes true, such as until nc -z postgres 5432. Nothing has crashed, so there is no error to read; the Pod simply waits. Check whether the dependency is reachable, and in particular run kubectl get endpoints <service>, since a Service with no ready backends resolves as a name while nothing answers. Always give wait loops a bounded number of attempts and a non-zero exit so the failure is visible.
How do init container resource requests affect scheduling?
The scheduler computes the Pod's effective request as the larger of the maximum single init container request and the sum of the app container requests, because init containers run sequentially and never at the same time as the app containers. A single init container with a large memory request can therefore make the whole Pod unschedulable even when the app containers are small, which is hard to spot from reading the manifest. Limits are still enforced per container, so an init container exceeding its own memory limit is OOMKilled with exit code 137.