Init Containers and Sidecar Containers
Article 18 left two conditions worth digging into further: Initialized and Ready. This article takes Initialized, the condition that reflects whether the init containers have finished running, and along the way covers its close relative: the sidecar container. Both are auxiliary containers in a pod, but they serve two opposite needs: an init container runs first then disappears, a sidecar runs alongside and stays. (Ready and the probe mechanism are saved for Article 20.)
Init container: finish preparing, then make way
An init container is a container that runs to completion before the application container is started. The docs put it concisely: "Init containers always run to completion" and "Each init container must complete successfully before the next one starts". They're declared in spec.initContainers, and if there are several, they run one after another in order — not in parallel.
Use an init container when there's work that must be done once, before the app runs: waiting for a dependency to be ready, fetching/preparing configuration or data into a volume the app will read, running a database migration, or setting file permissions. Splitting these out of the main container keeps the app image lean and the responsibilities clear.
One important difference from regular containers, quoted straight from the docs: "Regular init containers ... do not support the lifecycle, livenessProbe, readinessProbe, or startupProbe fields." Something that runs then finishes doesn't need liveness/readiness probes. But it does still support volumes, resource limits, and security context like a regular container.
Try a real init container
The following pod has an init container that writes data into a shared emptyDir, then the app container reads it back — the classic "init prepares, app consumes" pattern:
apiVersion: v1
kind: Pod
metadata: {name: init-demo}
spec:
volumes:
- {name: shared, emptyDir: {}}
initContainers:
- name: init-prep
image: busybox:1.36
command: ["sh","-c","echo 'du-lieu-do-init-chuan-bi' > /work/data.txt; sleep 15"]
volumeMounts: [{name: shared, mountPath: /work}]
containers:
- name: app
image: busybox:1.36
command: ["sh","-c","cat /work/data.txt; sleep 3600"]
volumeMounts: [{name: shared, mountPath: /work}]
Snapshot the pod while init is running:
kubectl get pod init-demo
kubectl get pod init-demo -o jsonpath='Initialized={..conditions[?(@.type=="Initialized")].status}{"\n"}app={.status.containerStatuses[0].state}{"\n"}'
NAME READY STATUS RESTARTS AGE
init-demo 0/1 Init:0/1 0 4s
Initialized=False
app={"waiting":{"reason":"PodInitializing"}}
STATUS: Init:0/1 (0 of 1 init containers done), Initialized=False, and the app container isn't running yet — it's Waiting with reason PodInitializing. This is what an init container guarantees: the app doesn't start while preparation isn't finished. Wait for init to complete and look again:
kubectl get pod init-demo
kubectl get pod init-demo -o jsonpath='Initialized={..conditions[?(@.type=="Initialized")].status}{"\n"}init={.status.initContainerStatuses[0].state.terminated.reason}{"\n"}'
kubectl logs init-demo -c app
NAME READY STATUS RESTARTS AGE
init-demo 1/1 Running 0 20s
Initialized=True
init=Completed
du-lieu-do-init-chuan-bi
The init container is Completed, Initialized flips to True, the app starts and reads exactly the data init just wrote through the shared volume. Article 18's Initialized condition now reveals what drives it.
When an init container fails
What happens if an init container fails? The docs: "If a Pod's init container fails, the kubelet repeatedly restarts that init container until it succeeds. However, if the Pod has a restartPolicy of Never ... Kubernetes treats the overall Pod as failed." Try one with an init that exits code 1 (the pod uses the default restartPolicy Always):
kubectl get pod init-fail
kubectl get pod init-fail -o jsonpath='initRestartCount={.status.initContainerStatuses[0].restartCount}{"\n"}app={.status.containerStatuses[0].state.waiting.reason}{"\n"}'
NAME READY STATUS RESTARTS AGE
init-fail 0/1 Init:Error 4 (66s ago) 100s
initRestartCount=4
app=PodInitializing
The init container dies and is repeatedly restarted (restartCount keeps climbing), and the app container is still stuck at PodInitializing: the pod doesn't budge until init succeeds. If init keeps dying, it enters exponential backoff just like Article 18 and STATUS becomes Init:CrashLoopBackOff. The operational lesson: a pod stuck forever at Init:... means the preparation is failing — read kubectl logs init-fail -c init-bad to find out why, don't look at the app container (it hasn't even run yet).
Sidecar container: runs with the app, stays for the lifetime of the pod
An init container disappears before the app runs. But often we need a container that runs alongside the app for the lifetime of the pod: a log-shipping agent, a network proxy, a config syncer. That's a sidecar.
People used to build sidecars by adding another container to spec.containers, which still works when the containers are "peers" and you don't need to control ordering. But that approach has a weakness: it doesn't guarantee the sidecar starts before the app, and when the pod shuts down the termination order is uncontrolled (the proxy might die before the app finishes closing connections).
Kubernetes solves this with the native sidecar, defined as an init container with restartPolicy: Always. Per the docs, this feature (the SidecarContainers feature gate) is enabled by default since v1.29 and GA/stable since v1.33; our cluster runs v1.36.1 so it works out of the box, nothing to enable.
apiVersion: v1
kind: Pod
metadata: {name: sidecar-demo}
spec:
initContainers:
- name: logshipper
image: busybox:1.36
restartPolicy: Always # <-- this line turns the init container into a sidecar
command: ["sh","-c","while true; do echo shipping logs; sleep 10; done"]
containers:
- name: app
image: busybox:1.36
command: ["sh","-c","echo app dang chay; sleep 3600"]
Why does putting the sidecar in initContainers not block the app like a regular init? The key is that a sidecar only needs to start, not to complete. The docs: "After a sidecar-style init container is running (the kubelet has set the started status for that init container to true), the kubelet then starts the next init container." That is, once the sidecar is started, the kubelet moves on, not waiting for it to finish (which it never does). The result is that the sidecar starts first, then the app runs alongside it:
kubectl get pod sidecar-demo
kubectl get pod sidecar-demo -o jsonpath='sidecar={.status.initContainerStatuses[0].name}:{.status.initContainerStatuses[0].state}{"\n"}app={.status.containerStatuses[0].name}:{.status.containerStatuses[0].state}{"\n"}'
NAME READY STATUS RESTARTS AGE
sidecar-demo 2/2 Running 0 21s
sidecar=logshipper:{"running":{"startedAt":"2026-05-23T15:19:51Z"}}
app=app:{"running":{"startedAt":"2026-05-23T15:19:51Z"}}
READY 2/2: the pod counts both the sidecar and the app, and both are running. Note that logshipper sits in initContainerStatuses (it is an init container by declaration) but behaves like a long-resident container: it runs forever, restarts independently if it dies, and unlike a regular init it is allowed to have probes.
Put the two startup models side by side to make it clear:
REGULAR INIT (blocking, runs to completion):
|== init-1 ==|== init-2 ==|========= app =========>
↑ Initialized=True, then app runs
SIDECAR (only needs "started", then runs in parallel):
|== sidecar started ==|···· sidecar keeps running ····|
|========= app ============>|
↑ app runs as soon as sidecar is started
Shutdown order, and why it's valuable
The subtlest point of the native sidecar lies in pod shutdown. The docs: "the kubelet postpones terminating sidecar containers until the main application container has fully stopped. The sidecar containers are then shut down in the opposite order of their appearance." That is, when the pod is deleted, the app stops first, the sidecar stops after and in reverse order. This is exactly what we want: a network proxy or log-shipping agent must stay alive until the app finishes closing connections / shipping its last logs, rather than dying first and leaving the app to send into the void.
Another consequence, important for Jobs (Article 27): a sidecar declared this way does not block the Job from completing. When the Job's main container finishes, the Job is considered done even though the sidecar is still running, and the kubelet shuts the sidecar down. A sidecar declared the old way (added to containers) would hang the Job forever because the sidecar never exits.
🧹 Cleanup
kubectl delete pod init-demo init-fail sidecar-demo
All are objects in the cluster — deleting them is clean. Manifests at github.com/nghiadaulau/kubernetes-from-scratch, directory 19-init-sidecar.
Wrap-up
Init containers and sidecars are two ways to add auxiliary containers to a pod for two opposite needs. An init container runs sequentially to completion before the app starts, drives the Initialized condition, and on failure the kubelet restarts it until it succeeds (or marks the pod Failed if restartPolicy: Never). A sidecar is an init container with restartPolicy: Always: it only needs to be started for the app to run alongside it, it stays for the lifetime of the pod, and on shutdown it terminates after the app in reverse order — the safe order for a proxy/agent. The "only needs started, not completed" difference explains why something declared in initContainers ends up running alongside the app.
Article 20 picks up the remaining condition from Article 18, Ready, along with the mechanism behind it: probes. We'll see the three kinds of probe (liveness, readiness, startup) decide when a container is considered alive, when it's ready to receive traffic, and when the kubelet should kill and restart it.