Christian Lehnert — Linux, Hacking & Faith

Init Containers - Run Something First, Then Get Out of the Way

Christian Lehnert2026-08-13~6 min read

Init Containers

Run First, Then Step Aside

A pod's normal containers all start together. An init container is
different: it runs to completion first, and only when it exits
successfully does the next one start. Chain several and they run in
order, one after another, each finishing before the next begins. When
the last init container is done, your application container starts.

That is the whole idea. An init container is setup that must happen
before the app runs, in its own container, guaranteed to finish and
guaranteed to succeed before the app gets to exist. If it fails, the
pod does not proceed. It retries the init container instead. The app
never starts against a half-finished environment, because the
environment is finished before the app is allowed to run.

Why Not Just Do It in the App

The obvious question is why this deserves a separate container instead
of a few lines at the top of your application's startup.

The answer is separation and guarantees. The setup runs in its own
image, with its own tools, and the app image does not have to carry
them. An init container that clones a git repo needs git; your app
image stays free of git. One that runs a database migration needs the
migration tool; your app does not ship it. You keep the application
image small and focused, and the one-time setup tooling lives where it
is used and nowhere else.

The guarantee is the other half. Because the init container must
succeed before the app starts, you cannot end up with an app running
against a database that was never migrated or a config that was never
fetched. The ordering is enforced by Kubernetes, not by hope. The app
does not need defensive code to check whether setup happened, because
it cannot run until it did.

What It Is Actually Good For

The honest list is shorter than people treat it.

Waiting for a dependency to be ready. An init container that blocks
until the database answers means the app starts into a world where its
database exists, instead of crash-looping through its first few
seconds while it waits. This is the most common good use, and it is
genuinely good.

Running a one-time setup that must happen before the app. A schema
migration, fetching config or secrets onto a shared volume, setting up
a data directory, cloning content the app will serve. Anything that
has to be done once, before the app, and has to be done right, fits
here cleanly.

Doing privileged setup the app should not do. An init container can
run with permissions the main app must never have, do the one
privileged thing at the start, tweak a kernel parameter, fix a volume
permission, and exit. The app then runs unprivileged. The dangerous
capability exists for a few seconds in a container that is gone before
the app starts, instead of living in the app for its whole lifetime.

The Line With Sidecars, Which Moved

For years there was a confusing overlap, and it is worth being precise
because Kubernetes recently drew the line sharply.

A plain init container runs once and exits. A sidecar runs alongside
the app for the pod's whole life. Those are different jobs: setup that
finishes versus a companion that stays. But people used to fake
sidecars with regular containers, and it caused real bugs, sidecars
that started at the same time as the app instead of before it, and
sidecars that kept a Job from ever completing because they never
exited.

Kubernetes fixed this by folding the sidecar into the init container
mechanism. An init container with a restart policy of Always is now a
native sidecar: it starts in init order, before the app, but instead
of exiting it keeps running alongside the app, and it shuts down after
the app is done. Stable since Kubernetes 1.33. So the rule is now
clean. No restart policy means a normal run-once init container. A
restart policy of Always means a sidecar that starts early and stays.
Same field in the manifest, two different jobs, and the ambiguity that
plagued the old approach is gone.

The practical upshot: if the thing needs to finish before the app,
it is an init container. If it needs to run beside the app, it is a
native sidecar. You no longer have to abuse one to get the other.

When Not To Use It

This is where most init container usage goes wrong. Not every bit of
setup belongs in one, and reaching for an init container reflexively
produces pods that are slower to start and harder to reason about than
they need to be.

If it belongs in the image, put it in the image. Installing packages,
downloading dependencies, compiling assets, that is build-time work.
Doing it in an init container means doing it on every single pod start,
forever, instead of once at build time. You are paying a startup tax on
every scale-up and every restart for work that should have been baked
into the image and never repeated. Build-time work goes in the
Dockerfile, not in an init container.

If the app can do it, and it is cheap, let the app do it. A trivial
check the application can run in a line of its own startup does not
need a whole extra container, image pull, and scheduling step wrapped
around it. Every init container is another image to pull, another thing
to start, another failure point, and more time before the app is up.
For genuinely trivial setup, the container is more overhead than the
work it does.

If it needs to run the whole time, it is not an init container. A thing
that has to keep running beside the app is a sidecar, and now that
native sidecars exist there is no reason to force it into the wrong
shape.

And do not chain a dozen of them. Each init container runs
sequentially, so ten of them is ten steps in series before your app
starts, and a slow one in the middle delays everything after it. A long
init chain is often a sign that setup that should have been one
build-time step or one application concern got scattered across many
containers because each felt easy to add.

The rule is the same as with sidecars: an init container earns its
place when setup genuinely must run first, must succeed first, and
does not belong in the image or the app. Outside that, it is startup
cost you are adding for no reason.

The Point

An init container runs to completion before your app, in its own
container, and the app does not start until it succeeds. That makes it
the right home for setup that must happen first and must not fail:
waiting on a dependency, a one-time migration, privileged prep the app
should not carry. Native sidecars now cover the run-alongside case, so
the two roles are finally distinct instead of blurred.

Use init containers for what must run first and succeed first. Keep
build-time work in the image, keep trivial checks in the app, use a
native sidecar for anything that has to stay running, and do not let
the init chain grow into a slow pile of steps nobody remembers adding.
Run something first, then get out of the way. That is the whole job.

Tagged:
#kubernetes #init-containers #design-patterns
← Back to posts