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:
- wait for a dependency (a name to resolve in DNS, a port to open)
- run database migrations (one per pod start - make them idempotent, or better, run them as a separate Job)
- fetch or render config into a shared
emptyDir(a scratch volume that lives as long as the pod, shared by its containers) - set permissions on a volume (
chownas root) so the app can run as non-root
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:
- Write init containers that wait or prepare, and read
Init:N/Mand-clogs. - Add a native sidecar (
restartPolicy: Alwaysin initContainers) sharing an emptyDir. - Choose init container vs sidecar for a task.