Kubernetes Init Containers: Database Migrations, Config Loading, and Dependency Waiting
Kubernetes Init Containers: Run Setup Before Your App Starts#
Init containers run to completion before your application container starts. They handle database migrations, configuration loading, dependency checks, and any setup that must finish before the main workload begins.
How Init Containers Work#
Pod startup sequence:
Init Container 1 → runs to completion ✓
↓
Init Container 2 → runs to completion ✓
↓
Init Container 3 → runs to completion ✓
↓
App Container(s) → start simultaneously
Key guarantees:
- Init containers run sequentially, one at a time
- Each must exit with code 0 before the next starts
- If any init container fails, Kubernetes restarts the pod (respecting
restartPolicy) - App containers only start after all init containers succeed
Basic Structure#
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
initContainers:
- name: init-db-migration
image: my-app:latest
command: ["python", "manage.py", "migrate"]
- name: init-wait-for-redis
image: busybox:1.36
command: ["sh", "-c", "until nc -z redis-svc 6379; do sleep 2; done"]
containers:
- name: app
image: my-app:latest
ports:
- containerPort: 8080
Use Case 1: Database Migrations#
Run schema changes before the application starts:
initContainers:
- name: migrate
image: my-app:latest
command: ["node", "migrate.js"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
Deploy new version
↓
Init container runs migrations
↓
Schema up to date?
├─ Yes → App container starts with new code
└─ No (migration failed) → Pod restarts, retries migration
Important: Migrations should be idempotent. The init container may run multiple times on retry.
Use Case 2: Waiting for Dependencies#
Block startup until upstream services are ready:
initContainers:
- name: wait-for-postgres
image: busybox:1.36
command:
- sh
- -c
- |
echo "Waiting for PostgreSQL..."
until nc -z postgres-svc 5432; do
echo "PostgreSQL not ready, retrying in 3s..."
sleep 3
done
echo "PostgreSQL is up"
- name: wait-for-kafka
image: busybox:1.36
command:
- sh
- -c
- |
until nc -z kafka-svc 9092; do sleep 3; done
Pod created
↓
wait-for-postgres: polling port 5432...
↓ postgres ready
wait-for-kafka: polling port 9092...
↓ kafka ready
App container starts (all dependencies confirmed)
Use Case 3: Configuration Loading#
Fetch config from external sources and write to a shared volume:
initContainers:
- name: fetch-config
image: curlimages/curl:latest
command:
- sh
- -c
- |
curl -s https://config-server/api/v1/config/production \
-H "Authorization: Bearer ${CONFIG_TOKEN}" \
-o /config/app-config.json
env:
- name: CONFIG_TOKEN
valueFrom:
secretKeyRef:
name: config-credentials
key: token
volumeMounts:
- name: config-volume
mountPath: /config
containers:
- name: app
image: my-app:latest
volumeMounts:
- name: config-volume
mountPath: /config
readOnly: true
volumes:
- name: config-volume
emptyDir: {}
The init container writes to /config, then the app container reads from the same path.
Use Case 4: Git Clone#
Clone a repository before serving it:
initContainers:
- name: git-clone
image: alpine/git:latest
command:
- git
- clone
- --depth=1
- https://github.com/org/static-site.git
- /site
volumeMounts:
- name: site-content
mountPath: /site
containers:
- name: nginx
image: nginx:alpine
volumeMounts:
- name: site-content
mountPath: /usr/share/nginx/html
readOnly: true
Resource Sharing Between Init and App Containers#
Shared Volumes#
Init Container Shared Volume App Container
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Writes config │ ──→ │ emptyDir{} │ ──→ │ Reads config │
│ Writes certs │ ──→ │ │ ──→ │ Reads certs │
│ Writes assets │ ──→ │ │ ──→ │ Serves assets │
└──────────────┘ └──────────────┘ └──────────────┘
Resource Requests#
Init containers affect pod scheduling differently:
initContainers:
- name: migrate
resources:
requests:
cpu: "500m"
memory: "512Mi"
- name: wait-deps
resources:
requests:
cpu: "50m"
memory: "32Mi"
containers:
- name: app
resources:
requests:
cpu: "250m"
memory: "256Mi"
Effective pod request = max(max(init requests), sum(app requests))
Init phase needs: max(500m, 50m) = 500m CPU, max(512Mi, 32Mi) = 512Mi
App phase needs: 250m CPU, 256Mi
Pod scheduled for: max(500m, 250m) = 500m CPU, max(512Mi, 256Mi) = 512Mi
Init containers do not add to the app container resource requirements because they run sequentially and finish before app containers start.
Failure Handling#
Restart Behavior#
spec:
restartPolicy: Always # Default for Deployments
initContainers:
- name: migrate
image: my-app:latest
command: ["python", "manage.py", "migrate"]
# If this exits non-zero → pod restarts → init containers run again
Init container fails (exit code 1)
↓
Pod status: Init:Error or Init:CrashLoopBackOff
↓
Kubernetes restarts pod (with backoff: 10s, 20s, 40s... up to 5m)
↓
All init containers run again from the beginning
Timeout Pattern#
Prevent init containers from blocking forever:
initContainers:
- name: wait-for-db
image: busybox:1.36
command:
- sh
- -c
- |
TIMEOUT=120
ELAPSED=0
until nc -z postgres-svc 5432; do
ELAPSED=$((ELAPSED + 3))
if [ $ELAPSED -ge $TIMEOUT ]; then
echo "Timed out waiting for PostgreSQL"
exit 1
fi
sleep 3
done
Init Containers vs Sidecar Containers#
Init Container Sidecar Container
Lifecycle Runs once, then exits Runs alongside app container
Ordering Sequential, before app Starts with app containers
Use cases Migrations, config fetch Logging, proxying, monitoring
Restart Restarts whole pod Restarted independently
Resource Not counted with app Counted with app resources
When to use which:
- Init container: One-time setup (migrate DB, clone repo, wait for dependency)
- Sidecar: Ongoing work (log shipping, service mesh proxy, metrics collection)
Sidecar Example for Comparison#
containers:
- name: app
image: my-app:latest
# This runs continuously alongside the app
- name: log-shipper
image: fluentd:latest
volumeMounts:
- name: logs
mountPath: /var/log/app
Production Patterns#
Migration with Rollback Safety#
initContainers:
- name: migrate
image: my-app:v2
command:
- sh
- -c
- |
# Run forward migration
python manage.py migrate --plan # dry run first
python manage.py migrate
# Verify migration succeeded
python manage.py check_migration_state
if [ $? -ne 0 ]; then
echo "Migration verification failed"
exit 1
fi
Multi-Step Initialization#
initContainers:
# Step 1: Wait for infrastructure
- name: wait-infra
image: busybox:1.36
command: ["sh", "-c", "until nc -z postgres-svc 5432; do sleep 2; done"]
# Step 2: Run migrations
- name: migrate
image: my-app:latest
command: ["python", "manage.py", "migrate"]
# Step 3: Seed cache
- name: warm-cache
image: my-app:latest
command: ["python", "manage.py", "warm_cache"]
# Step 4: Health pre-check
- name: verify
image: curlimages/curl:latest
command: ["sh", "-c", "curl -f http://config-svc/health || exit 1"]
wait-infra → migrate → warm-cache → verify → app starts
↓ ↓ ↓ ↓
postgres schema up cache hot config OK
Debugging Init Containers#
Check which init container is running or failed:
kubectl describe pod my-app
# Look at "Init Containers" section for status
kubectl logs my-app -c init-db-migration
# View logs from a specific init container
kubectl get pods
# STATUS column shows Init:0/3, Init:1/3, Init:Error, Init:CrashLoopBackOff
Common status meanings:
Init:0/2 First init container still running
Init:1/2 Second init container running (first succeeded)
Init:Error Init container exited with error
Init:CrashLoop Init container keeps failing, pod in backoff
PodInitializing All init containers done, app starting
Running Everything is up
Generate your Kubernetes deployment architecture at codelit.io →
Article #447 in the Codelit engineering series. Explore our full library of system design, infrastructure, and architecture guides at codelit.io.
Try it on Codelit
Cost Estimator
See estimated AWS monthly costs for every component in your architecture
GitHub Integration
Paste a repo URL and generate architecture from your actual codebase
Related articles
Try these templates
Build this architecture
Generate an interactive architecture for Kubernetes Init Containers in seconds.
Try it in Codelit →
Comments