OnCallReady

Lesson 15.35 · Kubernetes: Architecture & Workloads · 13 min read

Init containers and sidecars

In plain words

Before a play starts, stagehands set up the scenery, one job after another: first the backdrop, then the furniture. Only when they're done does the curtain rise. During the play, a prompter sits beside the stage the whole time, whispering lines when needed, and leaves only after the actors do.

In a pod, init containers are the stagehands: they run before the app, one at a time, in order, each to successful completion (wait for the database to resolve, run a migration, render config into an emptyDir), and the app never starts if one fails. Sidecars are the prompter: they run alongside the app for its whole life, sharing its network and volumes. Since Kubernetes 1.28, native sidecars are init containers with restartPolicy: Always: started before the app, stopped after it.

Why this matters

What you need to know already: pods and their containers (15.14), restartPolicy and CrashLoopBackOff (15.14), Jobs (15.24), DNS and nslookup (8.16), tail -F (1.5), Docker volumes (11.22).

An app often needs something done first (wait until the database name exists, run a database migration, prepare a config file) or something running beside it the whole time (ship its log file somewhere). Baking those into the app image mixes jobs and bloats it. A pod can hold extra containers for exactly these two cases: init containers (before) and sidecars (beside).

Init containers

spec.initContainers run before the app containers, one at a time, in order, each to successful completion. If one fails, it is restarted (under the pod's restartPolicy) and nothing after it runs.

spec:
  initContainers:
  - name: wait-for-db
    image: busybox:1.36
    command: ['sh', '-c', 'until nslookup db; do echo waiting for db; sleep 2; done']
  - name: migrate
    image: registry.lab/tools/migrate:1.0
    env:
    - name: DB_URL
      value: postgres://db:5432/shop
  containers:
  - name: app
    image: registry.lab/shop/api:2.3.0

(A migration is a script that changes a database's tables to the shape the new app version expects.) k get pod api -w (-w = watch: print a new line on every change) shows the STATUS column narrate it - Init:N/M means N of M init containers have finished:

# api = the pod above (the next mission builds one)
k get pod api -w
NAME   READY   STATUS     RESTARTS   AGE
api    0/1     Init:0/2   0          3s        wait-for-db running (db does not resolve)
api    0/1     Init:1/2   0          21s       it finished; migrate running
api    0/1     PodInitializing   0   27s       all init done, app starting
api    1/1     Running    0          36s
k logs api -c wait-for-db
Server:		10.96.0.10
Address:	10.96.0.10:53

** server can't find db.default.svc.cluster.local: NXDOMAIN

waiting for db
...

k logs POD -c NAME picks one container of a multi-container pod. Always -c <name> for an init container's logs - without it you get the app container, which has not started. (NXDOMAIN = "this name does not exist", 8.18.)

When an init container fails:

NAME   READY   STATUS                  RESTARTS      AGE
api    0/1     Init:Error              1 (3s ago)    12s
api    0/1     Init:CrashLoopBackOff   3 (22s ago)   1m

Use init containers for things that must be done before the app starts:

They can use a different image with tools the app image should not ship (no shells in production images, but the init image may have one).

Sidecars

A sidecar runs alongside the app for the pod's whole life, sharing its network namespace (same localhost - all containers of a pod share one network stack, 11.15) and volumes:

log shipper     tails files the app writes to a shared emptyDir
proxy           a reverse proxy (9.23) intercepting the app's traffic
auth proxy      a login proxy in front of an app with no auth
config reloader watches a mounted ConfigMap and signals the app

Before 1.28 a sidecar was just a second entry in containers. That had two real problems: startup order (the app might start before its proxy was ready and fail its first connections) and Jobs never finished (the Job waits for all containers to exit, and a log shipper never exits).

Native sidecars

Since 1.28 (beta and on by default since 1.29, GA - generally available, i.e. stable - in 1.33) a sidecar is an init container with restartPolicy: Always:

spec:
  initContainers:
  - name: log-shipper
    image: busybox:1.36
    restartPolicy: Always          # <- this makes it a sidecar
    command: ['sh', '-c', 'tail -F /var/log/app/app.log']
    volumeMounts:
    - name: logs
      mountPath: /var/log/app
  containers:
  - name: app
    image: busybox:1.36
    command: ['sh', '-c', 'while true; do echo "$(date) order processed" >> /var/log/app/app.log; sleep 5; done']
    volumeMounts:
    - name: logs
      mountPath: /var/log/app
  volumes:
  - name: logs
    emptyDir: {}

It starts in the init sequence (so it is up before the app), but the kubelet does not wait for it to exit - once it has started the next init container runs. It keeps running for the pod's life, is restarted if it dies, counts in READY (2/2), and is stopped after the main containers when the pod terminates. A Job with a native sidecar completes when its main container does.

# orders = the pod above (the sidecar mission builds it)
k get pod orders
NAME     READY   STATUS    RESTARTS   AGE
orders   2/2     Running   0          12s
k logs orders -c log-shipper
Wed Sep 23 10:00:13 UTC 2026 order processed
Wed Sep 23 10:00:18 UTC 2026 order processed
k logs orders
Defaulted container "app" out of: app, log-shipper (init)

Note kubectl logs without -c picks the first app container and tells you which ones it skipped.

Init container vs sidecar, in one sentence each

An init container does something once, to completion, before the app starts, and the app never starts if it fails. A sidecar does something continuously, beside the app, for the pod's whole lifetime.

What you can now do:

Why it helps

Both patterns are everywhere on a real platform: service mesh proxies, log shippers, Vault or Key Vault agents, config reloaders are sidecars; waiting for dependencies and volume permission fixes are init containers. Situations: a pod stuck at Init:0/2 and you read kubectl logs POD -c wait-for-db to see it can't resolve db; an app failing its first requests because it started before its proxy; a Job that never finishes because of a sidecar (native sidecars fix both). Knowing that kubectl logs without -c picks the first app container saves confusion, and "init container vs sidecar" is a common interview question.

FAQ

Why do I need -c to see an init container's logs?

Without -c, kubectl logs shows the first app container (and says which it defaulted to). During initialisation the app container hasn't started, so there's nothing to show. Use kubectl logs POD -c <init-container-name>, adding --previous if it has restarted.

What happens if an init container fails?

It's restarted according to the pod's restartPolicy, and nothing after it runs: later init containers and the app containers wait. The status shows Init:Error, then Init:CrashLoopBackOff. With restartPolicy: Never the pod fails instead. Fix the cause shown in that init container's logs.

Should database migrations run in an init container?

It works, but each pod start runs it, so with 10 replicas the migration runs up to 10 times, possibly concurrently. It must be idempotent and safe under concurrency. Often better: a separate Job run once per release before the rollout, which is also easier to observe and retry.

What is the difference between a native sidecar and a second container?

A second entry in containers starts at the same time as the app, with no ordering, and keeps a Job's pod from completing. A native sidecar, an init container with restartPolicy: Always, starts before the app (after its startup probe passes, if any), runs for the pod's life, is restarted if it dies, and is stopped after the main containers. A Job with native sidecars completes when its main container does.

Why use an init container with a different image?

It can carry tools the production app image shouldn't: shells, network utilities, migration binaries, or root to chown a volume so the app can run as non-root. The app image stays minimal, and the tools only exist during initialisation.

In an interview Junior

What is the difference between an init container and a sidecar?

Since 1.28 a native sidecar is an init container with restartPolicy: Always: it starts before the app, the kubelet does not wait for it to exit, it is restarted if it dies, it is stopped after the app, and a Job with one still completes. That fixed the two old problems of sidecars as plain containers: startup order and Jobs that never finished.

Debug either with k logs POD -c NAME - without -c you get the app container.

Also asked: A pod is stuck at Init:0/2. How do you debug it? · Why did Jobs with sidecar containers never complete before native sidecars? · What is an emptyDir volume used for?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.