Kubernetes Init Containers: A Practical Tutorial
Learn how Kubernetes init containers work, when to use them for setup tasks, and how to build robust pod initialization pipelines with real YAML examples.
What you'll learn
- ✓What init containers are and how they differ from sidecars
- ✓How init containers run sequentially before app containers
- ✓Common use cases like migrations and config rendering
- ✓How shared volumes pass data between init and app
- ✓Failure modes and how to debug them quickly
Prerequisites
- •Familiar with YAML and containers
What and Why
An init container is a container that runs to completion before the main containers in a Pod start. You can declare any number of init containers, and Kubernetes runs them one at a time, in order. If any init container exits non-zero, the kubelet restarts it according to the Pod’s restart policy and the main containers never start.
They exist because real applications often need a setup phase: wait for a database to be reachable, run schema migrations, fetch secrets from a vault, render a configuration file, or fix volume permissions. Putting that logic in the app image bloats it and couples concerns. Init containers let you keep the app image small and the setup explicit.
Mental Model
Picture the Pod lifecycle as two phases. Phase one is initialization: init containers run sequentially, each must succeed, and they share the Pod’s volumes and network namespace. Phase two is steady state: all main containers start simultaneously and run for the life of the Pod.
Init containers can use a different image than the app. That is the key insight. You can use a migrate image with database CLI tools to run migrations, then start a stripped-down app image that has no migration tooling at all.
Hands-on Example
This Pod waits for Postgres, runs migrations, then starts the API.
apiVersion: apps/v1
kind: Deployment
metadata:
name: catalog-api
spec:
replicas: 2
selector:
matchLabels:
app: catalog-api
template:
metadata:
labels:
app: catalog-api
spec:
volumes:
- name: config
emptyDir: {}
initContainers:
- name: wait-for-db
image: busybox:1.36
command:
- sh
- -c
- >
until nc -z postgres.data.svc.cluster.local 5432;
do echo waiting; sleep 2; done
- name: run-migrations
image: registry.example.com/migrator:1.2.0
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: catalog-db
key: url
command: ["/app/migrate", "up"]
- name: render-config
image: hashicorp/consul-template:0.36
args: ["-template", "/in/app.tmpl:/out/app.yaml", "-once"]
volumeMounts:
- name: config
mountPath: /out
containers:
- name: app
image: registry.example.com/catalog-api:2.0.1
volumeMounts:
- name: config
mountPath: /etc/catalog
Pod start
|
v
+-------------+ ok +----------------+ ok +----------------+ ok
| wait-for-db | -----> | run-migrations | -----> | render-config | ----+
+-------------+ +----------------+ +----------------+ |
v
+----------------+
| app starts |
+----------------+
The shared emptyDir volume named config lets the template renderer hand a finished config file to the app. If run-migrations fails, the kubelet retries it; the app never starts in a half-migrated state.
Common Pitfalls
The first pitfall is putting long-running work in an init container. They must exit. If you need a background helper that runs for the life of the Pod, that is a sidecar container, not an init container.
The second is forgetting that init containers run on every Pod restart. If your migration is not idempotent, two replicas starting at once can deadlock. Use a leader-election wrapper or a Job for one-time work.
The third is resource starvation. Init containers count against the Pod’s effective requests during scheduling. A heavy npm install in init can keep your Pod from being scheduled. Pre-bake dependencies into the image when you can.
Production Tips
Keep init container images small and trusted. They run with the same service account and often have access to sensitive secrets (database URLs, vault tokens). A bloated init image is a bigger attack surface.
Set explicit resources.requests and limits on init containers. The kubelet uses the maximum of init and app requests when reserving capacity, so under-requesting init containers does not save you anything but over-requesting them wastes node capacity.
Use init containers for permission fixes on persistent volumes. A tiny busybox container running chown -R 1000:1000 /data solves the classic “PV mounted as root” problem cleanly.
Log generously from init containers. They are easy to debug with kubectl logs <pod> -c <init-name>, but only if you actually print what step you are on.
Wrap-up
Init containers are the cleanest way to express a “do this before the app starts” requirement in Kubernetes. Use them for waits, migrations, config rendering, and volume prep. Keep them short, idempotent, and well-instrumented, and your Pods will start reliably every time.
Related articles
- Kubernetes Kubernetes Readiness vs Liveness Probes: A Practical Guide
Understand the difference between readiness and liveness probes in Kubernetes, when to use each, and how to configure them safely in production workloads.
- Kubernetes Kubernetes Secrets: Best Practices for Production
Practical patterns for managing Kubernetes Secrets safely: encryption at rest, external secret stores, RBAC scoping, rotation, and avoiding common leaks.
- Kubernetes Kubernetes Cluster Upgrades and Pod Eviction Explained
How Kubernetes cluster upgrades drain nodes, how pod eviction works, and how PodDisruptionBudgets and graceful shutdown keep workloads safe during upgrades.
- Docker Docker Compose vs Kubernetes: When to Use Which
A pragmatic comparison of Docker Compose and Kubernetes covering scope, operational cost, and the signals that tell you it is time to graduate.