iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Init Containers

Init containers run BEFORE the main containers in a Pod, sequentially, and must succeed before the pod is considered started. Use them for database migrations, config rendering, secret pre-fetch, network policy waits, schema setup — anything that must happen once before the app boots.

Order, restartPolicy, sidecar (1.29+), patterns

EXAMPLE
# 1) Minimal example — wait for a service before app starts
apiVersion: v1
kind: Pod
metadata:
    name: web
spec:
    initContainers:
        - name: wait-for-db
            image: busybox:1.36
            command:
                - sh
                - -c
                - |
                    until nc -z db.default.svc 5432; do
                        echo 'waiting for db…'
                        sleep 2
                    done
    containers:
        - name: web
            image: my-web:1.2
            ports: [{ containerPort: 8080 }]

# Sequential, sequential, sequential. Pod doesn't proceed until init succeeds.
# If init fails, restartPolicy decides retry behaviour (Always = restart).

# 2) Multi-step init — runs in order
spec:
    initContainers:
        - { name: clone-repo,   image: alpine/git:2.40,           command: ['git', 'clone', 'https://...', '/data'] }
        - { name: install-deps, image: node:20-alpine,            command: ['npm', '--prefix=/data', 'ci'] }
        - { name: build,        image: node:20-alpine,            command: ['npm', '--prefix=/data', 'run', 'build'] }
    containers:
        - { name: web,          image: node:20-alpine,            command: ['node', '/data/dist/server.js'], volumeMounts: [{ name: data, mountPath: /data }] }
    volumes:
        - { name: data, emptyDir: {} }

# 3) Database migration before app boots
spec:
    initContainers:
        - name: migrate
            image: my-app:1.2
            command: ['npm', 'run', 'db:migrate']
            env:
                - { name: DATABASE_URL, valueFrom: { secretKeyRef: { name: app-secrets, key: DATABASE_URL } } }
    containers:
        - name: app
            image: my-app:1.2
            envFrom: [{ secretRef: { name: app-secrets } }]

# 4) Fetch secrets from Vault / cloud provider
spec:
    initContainers:
        - name: vault-agent-init
            image: hashicorp/vault:1.16
            command: ['vault', 'agent', '-config=/etc/vault/config.hcl']
            volumeMounts:
                - { name: secrets, mountPath: /vault/secrets }
    containers:
        - name: app
            image: my-app:1.2
            volumeMounts:
                - { name: secrets, mountPath: /app/secrets, readOnly: true }
    volumes:
        - { name: secrets, emptyDir: {} }

# 5) Network policy ready / DNS pre-warming
spec:
    initContainers:
        - name: wait-net
            image: alpine:3.19
            command: ['sh', '-c', 'until nslookup api.example.com; do sleep 1; done']

# 6) Permissions fixing (legacy storage)
spec:
    initContainers:
        - name: fix-perms
            image: busybox
            command: ['sh', '-c', 'chown -R 1000:1000 /data && chmod -R u+rwX,g+rX /data']
            volumeMounts:
                - { name: data, mountPath: /data }
            securityContext: { runAsUser: 0 }   # only this init runs as root
    containers:
        - name: app
            image: my-app
            securityContext: { runAsUser: 1000 }
            volumeMounts:
                - { name: data, mountPath: /data }

# 7) Native sidecar pattern (Kubernetes 1.29+ stable)
# A sidecar is an INIT container with restartPolicy: Always — it stays running ALONGSIDE the main containers.
# Old pattern was 'just a regular container'; native sidecars get proper startup ordering.
spec:
    initContainers:
        - name: logging-sidecar
            image: fluent/fluent-bit:3.0
            restartPolicy: Always                # makes it a SIDECAR (not a one-shot init)
            volumeMounts:
                - { name: logs, mountPath: /logs }
        - name: setup
            image: busybox
            command: ['sh', '-c', 'mkdir -p /logs && touch /logs/app.log']
            volumeMounts:
                - { name: logs, mountPath: /logs }
    containers:
        - name: app
            image: my-app:1.2
            volumeMounts:
                - { name: logs, mountPath: /var/log/app }
    volumes:
        - { name: logs, emptyDir: {} }

# Sidecar init starts FIRST, stays running. Setup init runs to completion. App runs alongside sidecar.

# 8) Resources + limits
spec:
    initContainers:
        - name: migrate
            image: my-app:1.2
            command: ['npm', 'run', 'db:migrate']
            resources:
                requests: { cpu: 50m, memory: 256Mi }
                limits:   { cpu: 500m, memory: 512Mi }

# Init container resources can be HIGH because they run only once at startup.
# But total Pod resources sum the MAX of init + main during scheduling.

# 9) Order of operations
# 1. Volume mounts attached
# 2. initContainers run in declared order; each must Succeed
# 3. Main containers start in parallel
# 4. Readiness/liveness probes start gating after containers run
# 5. If init fails — Pod restarts according to restartPolicy (default Always)

# 10) Debugging
kubectl describe pod web                              # see Init events, exit codes
kubectl logs web -c wait-for-db                       # logs of a specific init container
kubectl logs web -c migrate --previous                 # last failed init

# 11) Anti-patterns
# • Long-running init container (5+ min) → slow rollouts; consider externalising the work
# • Init containers depending on the main app being ready → circular; restructure
# • Multiple init containers reading the SAME volume but expecting concurrent writes → sequential, won't work
# • Network calls in init container → handle retries; idempotency matters
# • Mounting Secret as env in init AND main with different values → confusing; use volumeMounts
# • Heavy init containers in DaemonSets → every node startup pays the cost

# 12) When to use a Job instead
# • Migrations as separate Job — runs OUTSIDE the pod lifecycle; idempotent retry; tracked in Jobs
# • Init container fine for fast, deterministic setup (waiting, fetching config)
# • Job better for one-time deployments and migrations gating a release

# 13) Common bugs
# • Init succeeded but secret not available at app start — refresh: env vs file mounts have different timing
# • restartPolicy: Always + failing init → infinite restart loop; fix the underlying error
# • Network race — DNS not ready yet → wait-for-* loop with timeout
# • forgotten 'restartPolicy: Always' on a native sidecar → behaves like a one-shot, app starts before sidecar ready
# • Init runs as root, app runs as non-root, but file ownership not adjusted → permission denied
# • Init pulls huge image → slow pod startup; use small images (busybox, alpine, distroless)
# • Image pull errors in init → ImagePullBackOff blocks the whole pod startup
# • Trying to share emptyDir contents between init containers AT THE SAME TIME — they run sequentially

Why it matters

Use init containers for sequential pre-flight tasks: wait for dependencies, run migrations, fetch secrets, fix file permissions. Each must succeed before the main containers start. Kubernetes 1.29+ makes native sidecars first-class (init container with restartPolicy: Always), so you can pair a logging or proxy sidecar with proper startup ordering.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
spec:
    initContainers:
        - name: migrate
            image: my-app:latest
            command: ["npm", "run", "migrate"]
    containers: …
Try it Yourself »

Discussion

Loading…