Init containers and sidecars
A Pod can hold more than your app. Init containers do setup work before your app starts, like a stage crew building the set before the show. Sidecars run next to your app the whole time, like a sound engineer who stays for the full concert. Both let you keep your app small and focused.
The picture
Init containers
You met one in chapter 14: a tiny container that waited for event-bus-srv before posts started. The rules are simple:
- They run before any normal container starts.
- They run one at a time, in the order you list them.
- Each must finish successfully (exit code 0) before the next one starts.
- They can use a different image from your app, with tools your app image does not need (a migration tool,
curl,git). - They can share files with the app through a volume such as
emptyDir(chapter 30).
Click the parts of this Pod:
apiVersion: v1
kind: Pod
metadata:
name: posts
spec:
initContainers:
- name: wait-for-bus
image: busybox:1.36
command: ["sh", "-c", "until nslookup event-bus-srv; do sleep 2; done"]
- name: migrate
image: micro-posts-migrate:1.0
command: ["npm", "run", "migrate"]
containers:
- name: posts
image: micro-posts:1.0
volumeMounts:
- name: shared
mountPath: /app/data
volumes:
- name: shared
emptyDir: {}
initContainers
A list, next to containers. Kubernetes runs wait-for-bus first. Only when it exits with 0 does migrate start. Only when that also succeeds does posts start.
containers
Your normal app. It starts with everything already prepared, so its own code can stay simple: no "wait and retry" logic at startup.
Shared volume
An init container can write files here (downloaded config, generated keys) and the app reads them. Init containers can mount the same volumes as the app.
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
posts 0/1 Init:0/2 0 5s ← first init container running
posts 0/1 Init:1/2 0 12s ← first done, second running
posts 1/1 Running 0 20s ← both done, app started
$ kubectl logs posts -c migrate # logs of one init container
Four classic init jobs
- name: wait-for-db
image: busybox:1.36
command: ["sh", "-c", "until nc -z db 5432; do echo waiting for db; sleep 2; done"]Do not start the app until the database answers. Without this, the app crashes a few times at startup and gets restarted, which works but looks messy and slows everything down.
- name: migrate
image: micro-posts:1.0 # same image, different command
command: ["node", "migrate.js"]Update the database tables before the new version starts. Careful: with 3 replicas, 3 Pods run the migration at the same time. Migrations must be safe to run more than once, or move them into a Job (chapter 36).
- name: download-model
image: curlimages/curl:8.8.0
command: ["curl", "-o", "/data/model.bin", "https://example.com/model.bin"]
volumeMounts:
- name: shared
mountPath: /dataDownload something big into a shared emptyDir, so the app image stays small.
- name: fix-perms
image: busybox:1.36
command: ["sh", "-c", "chown -R 1000:1000 /data"]
securityContext:
runAsUser: 0 # only this short step runs as root
volumeMounts:
- name: data
mountPath: /dataPrepare a volume with root rights in a short step, so the app itself never needs them (chapter 42).
When an init container fails
If an init container exits with an error, the Pod does not start. Kubernetes tries again, waiting longer each time, and the status tells you exactly where it is stuck:
posts 0/1 Init:0/2 0 3swait-for-busis running.posts 0/1 Init:1/2 0 10sThe event bus answered.
migratestarts.posts 0/1 Init:Error 1 14s posts 0/1 Init:CrashLoopBackOff 2 40sThe migration fails. Kubernetes restarts that init container (with the default
restartPolicy: Always), with longer and longer pauses. The app container never starts.$ kubectl logs posts -c migrate Error: column "author_id" already existsRead the init container's own logs with
-c. Fix the problem; the next retry succeeds and the app starts.
Sidecars
A sidecar is a second container that keeps running next to your app and adds a feature, without changing your app's code. Like a sidecar on a motorbike: attached, travels along, separate seat.
Log shipper
Reads the app's log files from a shared volume and sends them to your logging system.
Proxy
Handles HTTPS or authentication in front of the app, which keeps speaking plain HTTP on localhost.
git-sync
Keeps a folder in sync with a Git repo, e.g. the static files a web server shows.
Config reloader
Watches a ConfigMap file and tells the app to reload when it changes (chapter 18).
Each helper can be written in any language, updated separately, and reused next to many apps. Adapters and ambassadors (chapter 35) are two special kinds of sidecar.
Native sidecars
The old way to add a sidecar was simply a second entry under containers. That has two annoying problems: Kubernetes does not know which one is "the app", so the sidecar may start after the app (logs from the first seconds are lost), and in a Job (chapter 36) the Pod never finishes because the sidecar never exits.
The fix, stable since Kubernetes 1.33: put the sidecar under initContainers with restartPolicy: Always.
spec:
initContainers:
- name: log-shipper
image: fluent-bit:3.0
restartPolicy: Always # ← this makes it a sidecar, not a normal init step
volumeMounts: [{ name: logs, mountPath: /logs }]
containers:
- name: posts
image: micro-posts:1.0
volumeMounts: [{ name: logs, mountPath: /var/log/app }]
volumes:
- name: logs
emptyDir: {}
start: log-shipper ✓ (running) → posts starts
stop: posts stops first → log-shipper stops last
Job: task finishes → sidecar is stopped → Job Complete ✓Starts first, stops last, and never keeps a Job alive. It also restarts by itself if it crashes.
start: posts and log-shipper start in any order
stop: both get SIGTERM at the same time
Job: task finishes, sidecar keeps running → Job never completes ✗Still works for long-running apps, and you will see it in older YAML. Prefer the native style for new work.
Init, sidecar or postStart?
| Init container | Native sidecar | postStart hook | |
|---|---|---|---|
| Runs | Before the app, then exits | Before and during the app | Right after the app starts, in parallel |
| Own image | Yes | Yes | No, runs inside the app container |
| Order guaranteed | Yes | Starts before the app | No |
| Good for | Setup: wait, migrate, download | Helpers: logs, proxy, sync | Tiny extras (chapter 14) |
- Create the database tables before posts starts
A one-off step that must finish first.
- Ship log files to Elasticsearch all day
It must keep running as long as the app does.
- Wait until
event-bus-srvresolvesA gate: nothing else should start until it passes.
- Add HTTPS in front of an app that only speaks HTTP
A proxy that handles every request, all the time.
Recap
- Init containers run before the app, one at a time, and each must succeed.
- Use them for setup: waiting for dependencies, migrations, downloads, permissions.
- A failing init container blocks the Pod (
Init:CrashLoopBackOff); read its logs with-c. - Sidecars run next to the app and add features without touching its code.
- Native sidecars (
initContainers+restartPolicy: Always) start first, stop last, and let Jobs finish.
Quick check
Init:1/3 for ten minutes. What does that tell you?Show answer
The first of three init containers finished, and the second is still running or retrying. Run kubectl describe pod and kubectl logs <pod> -c <second-init-name> to see why.
Show answer
The sidecar is a normal container and never exits. Move it under initContainers with restartPolicy: Always, making it a native sidecar that is stopped when the task ends.
Init containers and sidecars: common questions
What is an init container in Kubernetes?
A container that runs to completion before the app containers start. Several init containers run one after another, and each must succeed first.
What is a native sidecar container?
A container listed under initContainers with restartPolicy: Always. It starts before the app, keeps running beside it, and stops after it, so Jobs can still complete.