Init Containers
Init containers are specialized containers that run sequentially and run-to-completion before the main application containers in a Pod start. Declared in
spec.initContainers[](parallel tospec.containers[]), each init container must exit with status zero before the next begins, and all must succeed before the Pod’s regular containers are even created. The Kubernetes documentation defines them plainly: “A Pod can have multiple containers running apps within it, but it can also have one or more init containers, which are run before the app containers are started” (kubernetes.io — Init Containers). They are the canonical hook for setup work that must happen exactly once per Pod incarnation — waiting on dependencies, pre-pulling data into shared volumes, applying database schema migrations, fetching secrets, generating configuration files — and they are the conceptual progenitor of Sidecar Containers (sidecars are simply init containers withrestartPolicy: Always, see Sidecar Containers for the rationale). The reason init containers exist as a distinct field rather than as “the first container inspec.containers” is twofold: ordering (regular containers in a Pod start in parallel; init containers do not), and failure semantics (a failed init container blocks the entire Pod from starting, surfacing asInit:ErrororInit:CrashLoopBackOff, whereas a failed regular container in a multi-container Pod can leave its peers running). This note covers the lifecycle contract, the resource-accounting formula (which is not a simple sum), and the failure modes operators encounter most often.
Mental Model
flowchart LR SCHED[Pod scheduled<br/>onto a Node] INIT1[initContainers[0]<br/>run to completion] INIT2[initContainers[1]<br/>run to completion] INITN[initContainers[N-1]<br/>run to completion] GATE{All init<br/>containers<br/>exited 0?} READY[Initialized=True<br/>condition set] MAIN[containers[*]<br/>start in parallel] FAIL[Init:Error or<br/>Init:CrashLoopBackOff<br/>depending on restartPolicy] SCHED --> INIT1 INIT1 -->|exit 0| INIT2 INIT2 -->|exit 0| INITN INITN -->|exit 0| GATE GATE -->|yes| READY READY --> MAIN INIT1 -.->|exit != 0| FAIL INIT2 -.->|exit != 0| FAIL INITN -.->|exit != 0| FAIL FAIL -.->|restartPolicy:<br/>Always or OnFailure| INIT1 FAIL -.->|restartPolicy: Never| FAIL
What this diagram shows. Init containers form a strictly-ordered, sequential prefix to the Pod’s life. The kubelet starts initContainers[0]; on exit-zero, it starts initContainers[1]; on exit-zero, the next; and so on until the whole list has completed. Only at that point does the Pod’s Initialized condition flip to True and the regular containers begin. If any init container exits non-zero, the kubelet restarts that init container if restartPolicy is Always or OnFailure (the default, Always, is what most Pods inherit). With restartPolicy: Never, a single init failure marks the entire Pod as Failed. The insight to extract is that init containers are the gate through which a Pod must pass before its real work begins: they are the place to encode “we cannot start until X is true” — X being any condition expressible as a script that exits zero when met. Once you internalize that init containers run a serial prefix and main containers run as a parallel suffix, every “how do I make Pod X wait for Y” question collapses to “put a script in an init container that polls for Y.”
Mechanical Walk-through
Lifecycle and ordering
A Pod’s lifecycle (kubernetes.io — Pod Lifecycle) proceeds through phases Pending → Running → Succeeded/Failed. The init phase lives inside Pending. The kubelet, upon admitting a Pod onto the node:
- Pulls the image for
initContainers[0](honoring the Pod’simagePullPolicy). - Starts the container with the same network namespace, the same volumes, and the same security context the Pod will use for its main containers — but only the
initContainers[0]runs at this point. No other container in the Pod is running yet. - Waits for exit. If exit code is
0, advances toinitContainers[1]. If exit code is non-zero, applies the Pod’srestartPolicy(see Failure Modes below). - Repeats for each subsequent init container in declaration order.
- Flips the
Initializedcondition toTrueonce the last init container exits zero. This is observable viakubectl get pod <name> -o yaml | yq '.status.conditions'. - Starts all
spec.containers[]in parallel — there is no equivalent ordering for regular containers. (If you need ordering among main containers, that’s a sidecar story, not an init-container one.)
The init-container container objects share the Pod’s namespace and volumes, so the canonical pattern of “write a config file in init, read it in main” works trivially: both containers mount the same emptyDir (or other) volume, and what init writes, main reads.
Probes and lifecycle hooks
Init containers do not support livenessProbe, readinessProbe, or startupProbe in the standard sense (they’re not long-running). They do not support lifecycle.preStop hooks (they exit on their own). They do support lifecycle.postStart, but it’s rarely useful — the container is expected to do its work and exit, so a postStart hook usually just races the main command. See Pod Probes for the probe model that applies to regular and sidecar containers.
The exception: native sidecar containers (init containers with restartPolicy: Always, stable since K8s 1.33 — see Sidecar Containers) do support probes. The probe semantics on a sidecar are the same as on a regular container; the “init container” naming is somewhat unfortunate because native sidecars are long-running. Mentally, distinguish run-to-completion init containers (the topic of this note) from long-running sidecars (a separate concept hosted in the same initContainers[] slot).
Resource accounting: not a simple sum
This is the most-misunderstood part of init containers. The Pod’s effective request for a given resource (CPU, memory) is not sum(initContainers) + sum(containers). Init containers run sequentially with main containers, never concurrently, so summing them would over-reserve. The actual formula, per the K8s docs (Resource sharing within containers):
The effective request/limit for a resource is the higher of:
- the sum of all app containers’ request/limit for that resource
- the highest single init container’s request/limit for that resource
Symbolically, for resource r:
Walking through that formula symbol by symbol: the inner sum totals up what the main containers will collectively need while running in parallel; the inner max picks out the single largest init container’s need (since init containers run one at a time, only the largest matters); the outer max takes whichever of the two is bigger because the scheduler must reserve enough capacity for whichever phase needs more. A consequence: you can declare a 4-GB init container without inflating the Pod’s effective request, as long as your main containers also collectively need 4 GB or more — the init container’s request is “absorbed” by the main containers’ request once init is done.
Native sidecars (init containers with restartPolicy: Always) do not participate in this max-based accounting. They run concurrently with main containers and are summed into the effective request like main containers. This is mentioned in KEP-753.
Status reporting
A Pod with init containers exposes a parallel status array:
.status.initContainerStatuses[]— one entry per init container; each hasstate(Waiting/Running/Terminated),ready,restartCount, and (for the terminated state)exitCode,reason, andstartedAt/finishedAt..status.containerStatuses[]— the regular containers’ statuses; populated only after init completes..status.conditions[]— includes theInitializedcondition (False during init, True after).
The short-form kubectl get pods output collapses init state into the STATUS column with values like Init:0/2 (no init containers completed yet out of 2), Init:1/2 (one done), Init:Error (the most recent init container exited non-zero), Init:CrashLoopBackOff (the kubelet is back-off-restarting a failing init container), or PodInitializing (last init container is running). These strings are the diagnostic signal you’ll see most often in the field.
Configuration / API Surface
A canonical Pod with two init containers — one waiting on DNS for a backing service, one applying database migrations — annotated line by line.
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
# restartPolicy applies to BOTH init and main containers.
# For Deployments, this is implicitly Always (you can't override).
# For Jobs, you'd set Never or OnFailure.
restartPolicy: Always
initContainers:
# ---------- Init container 1: wait for a Service to resolve ----------
- name: wait-for-postgres
image: busybox:1.36
# The whole thing is a polling loop: nslookup the Service DNS name
# every 2 seconds until it resolves. When the DNS record appears,
# nslookup exits 0 and this init container exits 0.
command:
- sh
- -c
- |
until nslookup postgres.default.svc.cluster.local; do
echo "waiting for postgres..."
sleep 2
done
# Tight resource bounds: this container is just polling DNS.
resources:
requests: { cpu: "10m", memory: "16Mi" }
limits: { cpu: "100m", memory: "64Mi" }
# ---------- Init container 2: apply database migrations ----------
- name: run-migrations
image: myorg/myapp-migrator:v1.4.2
# The migration tool exits 0 on success, non-zero on failure.
# If it fails, the kubelet will restart it per the Pod's
# restartPolicy: Always (the default), creating a CrashLoopBackOff.
command: ["/migrate", "--db=postgres.default.svc.cluster.local"]
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-credentials
key: password
# Migrations may need substantial memory for large schema diffs;
# this init container's request will dominate the effective Pod
# request via the max() formula above.
resources:
requests: { cpu: "500m", memory: "512Mi" }
limits: { cpu: "1", memory: "1Gi" }
containers:
# ---------- Main container: the app itself ----------
- name: app
image: myorg/myapp:v1.4.2
ports:
- containerPort: 8080
# When this container starts, both init containers have already
# succeeded: the Service is resolvable and migrations are applied.
# The app can assume those invariants without checking.
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 5
resources:
requests: { cpu: "200m", memory: "256Mi" }
limits: { cpu: "1", memory: "512Mi" }Effective request calculation for the Pod above. Memory: max(sum_main = 256Mi, max_init = max(16Mi, 512Mi) = 512Mi) = 512Mi. CPU: max(sum_main = 200m, max_init = max(10m, 500m) = 500m) = 500m. The migrator’s spike is what the scheduler must find capacity for; the app’s smaller request is “free” once init is done.
Failure Modes
-
Image pull failure on an init container. Surfaces as
Init:ImagePullBackOfforInit:ErrImagePull. Same cause as for regular containers (missing image, bad credentials, network), but the entire Pod stalls inPending— no main container ever runs. Diagnostic:kubectl describe pod <name>and look at the init container’sEvents. -
Non-zero exit code with
restartPolicy: Always. The kubelet restarts the init container, capped by exponential backoff (5s, 10s, 20s, 40s, 80s, 160s, 300s cap per the standardCrashLoopBackOffschedule). The Pod status showsInit:CrashLoopBackOffand the affected init container’srestartCountincrements. The main containers never start. If the init logic depends on an external resource (e.g., waiting on a Service that doesn’t exist), the Pod will loop forever — set a reasonable retry budget in the init container’s script, or the Pod becomes a permanent zombie. -
Non-zero exit code with
restartPolicy: Never(typical for Jobs). The Pod transitions toFailed. No retry; the Job controller will create a new Pod (subject tobackoffLimit) if this is a Job. Readingstatus.initContainerStatuses[i].state.terminated.exitCodegives the failure detail. -
Init container reads from a volume the main container will write to. The volume sequencing is implicit — both containers mount the same volume, but if the init container expects the main container’s data, it will not be there yet. This is a logic error: init runs before main, never with concurrent access to main’s mutations.
-
Misunderstanding the resource formula leads to over-provisioning. A common over-correction is to set huge requests on init containers expecting them to be summed; in fact only the max init request counts (against the sum of main). Conversely, declaring a tiny request on a memory-heavy init (e.g., a
go testmigrator) gets the init container OOM-killed if the main containers’ combined request is also small — there’s no scheduler-side guarantee that the largest init’s needs are met if the main containers’ sum is smaller than the actual init demand and the node is tight. The fix: ensure the init container’s own request reflects its real need. -
Long-running operations modeled as init containers. Init containers block the Pod from starting; if a long migration takes 20 minutes, the Pod is in
Init:Runningfor 20 minutes and consumes no service traffic. This is fine for migrations but wrong for things like “warm the cache” — those want to run concurrently with the main container and influence readiness, which is what a sidecar plus readiness probe is for. -
Init containers as a substitute for ConfigMap or Secret. Init containers that
curla config from an HTTP endpoint at boot couple the Pod’s startup to that endpoint. Prefer mounting a ConfigMap or projected volume; reserve init containers for cases where the config is genuinely dynamic per-Pod (e.g., depends on the Pod’s IP). -
Forgetting that init containers don’t have ports. You can declare a
containerPorton an init container, but it’s not useful — by the time the main containers run (and a Service can route to them), the init container has exited. The port declaration is benign noise.
Alternatives and When to Choose Them
-
Main-container entry-point script. Bake the “wait for X; then start the app” logic into the main container’s image. Pros: one fewer container, all logic in one place, no resource-accounting subtleties. Cons: couples application image to environmental concerns (DNS readiness, migration logic), bloats image, mixes concerns. Choose for tiny throwaway containers; reject for production services.
-
Sidecar container (Sidecar Containers). Use when the supporting logic must continue running alongside the main container (mesh proxy, log shipper, secret rotator), not just at startup. Native sidecars (init containers with
restartPolicy: Always, stable in 1.33) are the modern way; cross-link Sidecar Containers for the full story. -
Operator-driven precondition checks. For complex cross-Pod orchestration (“wait until the database StatefulSet has at least one ready replica before scheduling any frontend Pod”), an operator that gates the Deployment via a status field is more flexible than per-Pod init containers. Choose when the precondition is fundamentally a controller-level concern, not a per-Pod check.
-
Readiness probes on the main container. Some teams encode “is dependency X up?” into the main container’s readiness probe instead of an init container. This works, but it leaves the main container running (consuming CPU on its own startup, potentially crashing on missing dependencies); init containers cleanly isolate the wait. Choose readiness for “I can serve some traffic but not all”; choose init for “I cannot start at all.”
-
Job + activeDeadlineSeconds. For one-shot migration jobs that should run before a Deployment is rolled out, a separate Job — chained via ArgoCD sync waves or Helm hooks — is a stronger pattern than an init container inside every Pod replica. The Job runs once; the init container would run once per Pod replica (potentially racing). Choose the Job for cluster-singleton operations.
Production Notes
-
Spotify’s database migrations (per their engineering blog and KubeCon talks): historically used a Helm
post-installJob for schema migrations, not an init container, precisely because they wanted one migration per release, not one per Pod replica. The lesson: if your migration is idempotent and per-Pod-safe, init containers are fine; if not, lift the work out to a Job. -
Istio’s
istio-initcontainer (in classic sidecar-injection mode, pre-CNI) is the canonical “init container for plumbing” example: it programs iptables rules in the Pod’s network namespace to redirect traffic through the Envoy sidecar. It exits after applying the rules; the rules persist in the network namespace. This is the textbook use of an init container for a one-shot namespace mutation. -
The “wait-for” pattern (init container running
nslookup/netcat/curlin a loop until a dependency is up) is so common that several reusable images exist (e.g.,groundnuty/k8s-wait-for). The pattern is so widespread that the Kubernetes documentation itself uses it as the primary example (init-containers). -
The OOM gotcha. A memory-heavy init container that exceeds its declared
limits.memorygets OOM-killed, which the kubelet sees as a non-zero exit. IfrestartPolicy: Always, the Pod loopsInit:CrashLoopBackOffforever. Production teams typically wrap their init scripts withset -eand explicit memory headroom inlimits, and alert onInit:CrashLoopBackOffseparately from main-containerCrashLoopBackOffin their monitoring. -
Failure surface area. Every init container is a hard dependency that can break Pod startup. Teams that initially loved init containers (clean separation of concerns!) often regret it after the third Sev-2 where a flaky
nslookupagainst a Service IPv6 record blocked a whole Deployment rollout. The pragmatic guidance: each init container should be either trivial and reliable (small image, simple logic) or idempotently retryable (so backoff-restart converges); avoid medium-complexity init containers that can fail in interesting ways.
See Also
- Pod — the parent resource; init containers are a field on the Pod spec
- Pod Lifecycle — where init containers fit in
Pending → Running - Sidecar Containers — init containers’ long-running sibling (init container with
restartPolicy: Always) - Ephemeral Containers — the third kind of container; debugging only
- Pod Probes — liveness/readiness/startup; do not apply to init containers
- ReplicaSet / Deployment / StatefulSet / Job / CronJob — workloads that template Pods (and thus init containers)
- Resource Requests and Limits — the request/limit fields init containers participate in
- QoS Classes — derived from requests/limits; init containers are part of the calculation via the max-formula
- ConfigMap / Secret / Projected Volume — usually better than init-container-driven config
- Operator Pattern — domain-specific alternative to init-container preconditions
- Kubernetes MOC — umbrella index