cleanimg-init enables CleanStart images to run initialization tasks in Kubernetes initContainers—perfect for database migrations, configuration generation, dependency setup, and service coordination before the main application starts. InitContainers run sequentially before app containers, ensuring dependencies are ready before the application begins. CleanStart pre-configures them with proper signal forwarding, liveness probes, and timeout management.
The following diagram illustrates the Kubernetes init container lifecycle with sequential execution of init tasks before the main application starts:
graph TB A["Pod<br/>Created"] -->|Phase 1| B["Init 1:<br/>DB Migration"] B -->|Success| C["Init 2:<br/>Wait Postgres"] C -->|Success| D["Init 3:<br/>Wait Redis"] D -->|Success| E["Init 4:<br/>Generate Config"] E -->|Success| F["Init 5:<br/>Setup Volumes"] B -->|Failure| G["Pod:<br/>CrashLoopBackOff"] C -->|Failure| G D -->|Failure| G E -->|Failure| G F -->|Failure| G F -->|All Init Success| H["Phase 2:<br/>Main Container"] H -->|Start| I["App Container<br/>Running"] I -->|Healthy| J["Pod:<br/>Ready"] I -->|Unhealthy| K["Liveness<br/>Probe Failed"] K -->|Restart| H J -->|Shutdown| L["Pod:<br/>Terminating"] L -->|Cleanup| M["Pod:<br/>Terminated"]When to Use initContainers
InitContainers are ideal for one-time setup tasks that must complete successfully before the main application starts. They shine in scenarios involving database schema migrations where you need to run a migration tool once, ensure it succeeds, and then start the application. They work well for generating configuration files dynamically at pod startup time based on environment variables or secrets. They can download or prepare runtime data that the application needs—downloading compiled models for an ML application, fetching deployment artifacts, or preparing caches. They are essential for waiting for service dependencies: rather than having the application implement retry logic and fail gracefully, an initContainer can block until Redis is healthy, PostgreSQL is accepting connections, or Kafka is ready, ensuring the application starts only when all dependencies are satisfied. They can check the health of upstream services and validate configuration before the application starts. They can initialize file permissions and verify that volume mounts are writable and correctly configured.
InitContainers are not appropriate for long-running processes. If you need a process that runs continuously alongside your application—a logging agent, a monitoring sidecar, a proxy—use sidecar containers instead, which run alongside the main container throughout the pod's lifetime. InitContainers are not appropriate for operations that should happen continuously. If you need something to run periodically (log rotation, cache cleanup, reporting), use Kubernetes CronJobs instead of initContainers. InitContainers are not appropriate for real-time state management. If your application needs to dynamically respond to state changes, that logic belongs in the application itself, not in an initContainer that runs once at startup.
InitContainer Patterns
Pattern 1: Database Migration
apiVersion: apps/v1kind: Deploymentmetadata: name: myappspec: template: spec: initContainers: # Run database migrations before app starts - name: db-migrate image: us-docker.pkg.dev/my-project/prod/myapp:latest command: - /usr/local/bin/migrate - --database - postgresql://user:pass@postgres:5432/mydb - --direction - up env: - name: DATABASE_URL valueFrom: secretKeyRef: name: db-credentials key: url resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" containers: - name: app image: us-docker.pkg.dev/my-project/prod/myapp:latest # App starts only after migration completes successfullyThe workflow proceeds as follows: the pod starts, the initContainer db-migrate runs the migration tool, if the migration succeeds the process continues, if the migration fails the pod enters CrashLoopBackOff, and on success the main app container starts.
Pattern 2: Wait for Dependency
initContainers: - name: wait-for-postgres image: us-docker.pkg.dev/my-project/prod/utils:latest command: - /bin/sh - -c - | until pg_isready -h postgres.default.svc.cluster.local -p 5432; do echo "Waiting for PostgreSQL..." sleep 2 done echo "PostgreSQL is ready" env: - name: PGPASSWORD valueFrom: secretKeyRef: name: db-credentials key: password - name: wait-for-redis image: us-docker.pkg.dev/my-project/prod/utils:latest command: - /bin/sh - -c - | until redis-cli -h redis.cache.svc.cluster.local -a $REDIS_PASSWORD PING > /dev/null 2>&1; do echo "Waiting for Redis..." sleep 2 done echo "Redis is ready" env: - name: REDIS_PASSWORD valueFrom: secretKeyRef: name: redis-credentials key: passwordThis pattern is simpler than application-level retry logic. Services will retry automatically.
Pattern 3: Generate Configuration
initContainers: - name: generate-config image: us-docker.pkg.dev/my-project/prod/config-generator:latest command: - /usr/local/bin/generate-config - --environment - production - --output-dir - /config volumeMounts: - name: config-volume mountPath: /config env: - name: API_KEY valueFrom: secretKeyRef: name: api-keys key: prod-key - name: LOG_LEVEL value: info resources: requests: memory: "128Mi" cpu: "50m" limits: memory: "256Mi" cpu: "200m" containers: - name: app image: us-docker.pkg.dev/my-project/prod/myapp:latest volumeMounts: - name: config-volume mountPath: /etc/app volumes: - name: config-volume emptyDir: {} # ephemeral, discarded when pod terminatesThe process works as follows: the config generator runs and writes to the /config volume, the app container starts with config available at /etc/app, and the config persists only for the pod lifetime.
Pattern 4: File Permissions and Setup
initContainers: - name: init-app-volume image: us-docker.pkg.dev/my-project/prod/myapp:latest command: - /bin/sh - -c - | # Create application directories with correct permissions mkdir -p /app/data /app/logs /app/tmp chmod 700 /app/data chmod 755 /app/logs chmod 777 /app/tmp # Verify volume is writable touch /app/data/.ready echo "App volume initialized" volumeMounts: - name: app-data mountPath: /app securityContext: runAsUser: 65532 allowPrivilegeEscalation: false containers: - name: app image: us-docker.pkg.dev/my-project/prod/myapp:latest volumeMounts: - name: app-data mountPath: /app volumes: - name: app-data persistentVolumeClaim: claimName: app-data-pvcComplete Real-World Example: Web App + PostgreSQL
apiVersion: apps/v1kind: Deploymentmetadata: name: web-app namespace: productionspec: replicas: 3 selector: matchLabels: app: web-app template: metadata: labels: app: web-app spec: # InitContainers run in order before main containers initContainers: # 1. Wait for PostgreSQL - name: wait-for-db image: us-docker.pkg.dev/my-project/prod/postgres:15-client command: - /bin/sh - -c - | echo "Waiting for PostgreSQL..." until pg_isready \ -h postgres.database.svc.cluster.local \ -p 5432 \ -U webapp; do sleep 2 done echo "PostgreSQL is ready" env: - name: PGPASSWORD valueFrom: secretKeyRef: name: postgres-credentials key: password resources: requests: memory: "64Mi" cpu: "50m" # 2. Run database migrations - name: db-migrate image: us-docker.pkg.dev/my-project/prod/web-app:v1.2.3-signed command: - /usr/local/bin/migrate - --source - file:///migrations - --database - "postgresql://webapp:$(DB_PASSWORD)@postgres.database.svc.cluster.local:5432/webapp?sslmode=require" - up env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: postgres-credentials key: password volumeMounts: - name: migrations mountPath: /migrations resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" # 3. Generate configuration from secrets - name: generate-config image: us-docker.pkg.dev/my-project/prod/config-generator:latest command: - /bin/sh - -c - | cat > /etc/app/config.yaml <<'EOF' server: host: 0.0.0.0 port: 8080 database: host: postgres.database.svc.cluster.local port: 5432 name: webapp username: webapp ssl: true logging: level: ${LOG_LEVEL} format: json EOF echo "Configuration generated" env: - name: LOG_LEVEL value: "info" volumeMounts: - name: app-config mountPath: /etc/app resources: requests: memory: "128Mi" cpu: "50m" # Main application container containers: - name: app image: us-docker.pkg.dev/my-project/prod/web-app:v1.2.3-signed ports: - name: http containerPort: 8080 protocol: TCP env: - name: CONFIG_PATH value: /etc/app/config.yaml - name: DATABASE_PASSWORD valueFrom: secretKeyRef: name: postgres-credentials key: password volumeMounts: - name: app-config mountPath: /etc/app # Startup probe: app is healthy? startupProbe: httpGet: path: /health/startup port: http initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 30 # Gives app 150 seconds (30 * 5) to start # Liveness probe: is app still alive? livenessProbe: httpGet: path: /health/live port: http initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 # Readiness probe: can handle traffic? readinessProbe: httpGet: path: /health/ready port: http initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 1 resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" securityContext: runAsNonRoot: true runAsUser: 65532 capabilities: drop: - ALL readOnlyRootFilesystem: true allowPrivilegeEscalation: false # Graceful shutdown configuration terminationGracePeriodSeconds: 30 # Pod disruption budget affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - web-app topologyKey: kubernetes.io/hostname # Volumes volumes: - name: app-config emptyDir: {} - name: migrations configMap: name: db-migrations defaultMode: 0755 ---# ConfigMap with database migrationsapiVersion: v1kind: ConfigMapmetadata: name: db-migrations namespace: productiondata: 001_initial_schema.sql: | CREATE TABLE users ( id SERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sessions ( id UUID PRIMARY KEY, user_id INTEGER REFERENCES users(id), token_hash VARCHAR(255) UNIQUE NOT NULL, expires_at TIMESTAMP NOT NULL ); 002_add_user_roles.sql: | ALTER TABLE users ADD COLUMN role VARCHAR(50) DEFAULT 'user'; CREATE INDEX idx_users_role ON users(role);Signal Forwarding and Graceful Shutdown
InitContainers handle signals properly through the use of trap handlers. When you define an initContainer that performs important operations, you should implement a signal handler to ensure clean shutdown.
initContainers: - name: migration image: us-docker.pkg.dev/my-project/prod/myapp:latest command: - /bin/sh - -c - | trap 'echo "SIGTERM received, cleaning up..."; exit 0' SIGTERM /usr/local/bin/migrate --up EXIT_CODE=$? echo "Cleanup..." # Perform cleanup exit $EXIT_CODEWhen Kubernetes sends SIGTERM during initContainer execution, the container receives SIGTERM, the container cleanup code runs, the container exits cleanly, and the pod terminates without retrying the failed initContainer with pending failure.
Debugging InitContainers
View initContainer logs
# All init containers that rankubectl logs POD_NAME -n NAMESPACE --all-containers=true --prefix=true # Specific initContainerkubectl logs POD_NAME -n NAMESPACE -c migration # If container already terminatedkubectl logs POD_NAME -n NAMESPACE -c migration --previousDescribe pod to see status
kubectl describe pod POD_NAME -n NAMESPACE # Output will show:# Init Containers:# migration:# State: Terminated# Reason: Completed# Exit Code: 0# Containers:# app:# State: RunningCheck initContainer exit code
kubectl get pod POD_NAME -n NAMESPACE -o jsonpath='{.status.initContainerStatuses[0].state.terminated.exitCode}'# 0 = success# non-zero = failedIf initContainer fails
# See why it failedkubectl describe pod POD_NAME -n NAMESPACE | grep -A 10 "Init" # Check logs for error messagekubectl logs POD_NAME -n NAMESPACE -c CONTAINER_NAME # If pod is in CrashLoopBackOff due to init failure:# 1. Fix the underlying issue (e.g., database not ready)# 2. Delete the pod to trigger retrykubectl delete pod POD_NAME -n NAMESPACE # Or view eventskubectl get events -n NAMESPACE --sort-by='.lastTimestamp' | tail -20Performance Tips
initContainers: # 1. Set appropriate resource requests/limits - name: fast-init resources: requests: memory: "64Mi" # Small footprint cpu: "50m" limits: memory: "128Mi" # 2. Use fast, minimal images (CleanStart Alpine) image: us-docker.pkg.dev/.../init:latest # Should be <100MB # 3. Parallelize where possible (multiple initContainers) # - They run sequentially, but can use shared volumes # - Design independent init tasks for parallelization # 4. Add startup probes with generous timeouts startupProbe: periodSeconds: 1 # Check frequently during startup failureThreshold: 300 # 5 minutes totalError Handling
initContainers: - name: critical-init image: us-docker.pkg.dev/my-project/prod/init:latest command: - /bin/sh - -c - | set -e # Exit on any error echo "Step 1: Validate configuration" /usr/local/bin/validate-config || exit 1 echo "Step 2: Check dependencies" /usr/local/bin/check-deps || exit 2 echo "Step 3: Initialize storage" /usr/local/bin/init-storage || exit 3 echo "All initialization steps completed successfully" exit 0 # If exit code != 0, pod enters CrashLoopBackOff# Pod will restart initContainer after delayKubernetes Await Pattern
For complex initialization sequences, you can use a specialized await tool that manages multiple service dependencies:
initContainers: - name: await-dependencies image: us-docker.pkg.dev/my-project/prod/await:latest command: - /usr/local/bin/await - --service - postgres.database.svc.cluster.local:5432 - --service - redis.cache.svc.cluster.local:6379 - --service - kafka.messaging.svc.cluster.local:9092 - --timeout - "5m" # Waits for ALL services to be healthy before proceedingThe await tool tests TCP connectivity to specified services, retries with exponential backoff, times out and fails if services are not ready, and logs progress for debugging purposes.
Sharing Data Between InitContainers
When you have multiple init containers that need to share data or pass information to the main application container, you can use shared volumes. Init containers execute sequentially, allowing data created by one init container to be consumed by another, and volumes can be accessed by all containers in the pod.
spec: initContainers: - name: init-1 image: myimage:latest command: ["/bin/sh", "-c", "echo 'data' > /shared/output.txt"] volumeMounts: - name: shared mountPath: /shared - name: init-2 image: myimage:latest command: ["/bin/sh", "-c", "cat /shared/output.txt && echo 'processed' > /shared/done.txt"] volumeMounts: - name: shared mountPath: /shared containers: - name: app volumeMounts: - name: shared mountPath: /shared volumes: - name: shared emptyDir: {}Best Practices
The following practices will help you use init containers effectively. Keep init containers fast since app startup depends on completion. Use timeouts wisely by providing startup probes with generous time allowances of 5-10 minutes. Don't hide errors—fail fast and log clearly so you can diagnose issues. Test locally first using docker run with the same commands before deploying to Kubernetes. Version control migrations by storing them in ConfigMap or persistent storage. Monitor init performance since slower init means slower deployments. Avoid long-running tasks by using background jobs or CronJobs instead. Share volumes carefully since emptyDir doesn't persist across pods.
