Skip to content
C Codeloom
Kubernetes

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.

·4 min read · By Codeloom
Intermediate 9 min read

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   |
                                                            +----------------+
Sequential init container execution before app start

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.