A Kubernetes pod can run more than one container. Two patterns govern how these containers are used: Init Containers run sequentially before your app starts β used for setup tasks and dependency checks. Sidecars run alongside your app for the entire pod lifetime β used for logging, proxying, and metrics collection. Understanding both patterns eliminates entire categories of startup bugs and observability gaps.
+++
Managing Multi-Container Pods with Init Containers and Sidecars
Why Multiple Containers in One Pod
A pod is not just a container β it is a group of containers that share a network namespace and optional storage volumes. All containers in a pod have the same IP address and can reach each other on localhost.
Pod: payments-api +--------------------------------------------------+| Shared network (localhost) || Shared volumes (/var/log, /tmp/config) || || +------------------+ +---------------------+ || | payments-api | | log-forwarder | || | (main app) | | (sidecar) | || | port 8080 | | reads /var/log/*.log | || +------------------+ +---------------------+ || |+--------------------------------------------------+The two containers do not need to know about each other β they cooperate through shared filesystem paths and localhost networking.
Init Containers β Sequential Setup Before Your App Starts
An Init Container runs to completion before any regular container in the pod starts. If the init container fails, Kubernetes restarts it (subject to the pod's restartPolicy) until it succeeds. The main app container never starts until all init containers have completed successfully.
Pod startup sequence with init containers: [Init: 0/2] init-wait-for-db β runs β SUCCESS[Init: 1/2] init-run-migration β runs β SUCCESS[Running] payments-api β starts β Only after both inits complete If init-wait-for-db fails:[Init: 0/2] init-wait-for-db β runs β FAILS β retry β retry β retry... payments-api never startsInit Container Pattern 1 β Wait for a Dependency
The most common init container. Your app requires a database or a config service to be ready. Without this check, your app starts, fails to connect, and enters CrashLoopBackOff.
1apiVersion: apps/v12kind: Deployment3metadata:4 name: payments-api5 namespace: production6spec:7 replicas: 38 selector:9 matchLabels:10 app: payments-api11 template:12 spec:13 initContainers:14 - name: init-wait-for-postgres15 image: busybox:1.3616 command:17 - sh18 - -c19 - |20 echo "Waiting for PostgreSQL to be ready..."21 until nc -z postgres-service 5432; do22 echo "PostgreSQL not ready β retrying in 5s"23 sleep 524 done25 echo "PostgreSQL is ready"2627 - name: init-wait-for-redis28 image: busybox:1.3629 command:30 - sh31 - -c32 - |33 until nc -z redis-service 6379; do34 sleep 235 done36 echo "Redis is ready"3738 containers:39 - name: payments-api40 image: registry.razorpay.in/payments-api:v2.5.141 ports:42 - containerPort: 80801# Watch init containers progress2kubectl get pod payments-api-7d9f8b-xk2p9 -n production -w3 4# Output:5# NAME READY STATUS RESTARTS AGE6# payments-api-7d9f8b-xk2p9 0/1 Init:0/2 0 5s7# payments-api-7d9f8b-xk2p9 0/1 Init:1/2 0 12s8# payments-api-7d9f8b-xk2p9 0/1 PodInitializing 0 15s9# payments-api-7d9f8b-xk2p9 1/1 Running 0 16s10 11# Read init container logs specifically12kubectl logs payments-api-7d9f8b-xk2p9 \13 -c init-wait-for-postgres \14 -n productionInit Container Pattern 2 β Fetch Config at Startup
An init container downloads a config file from a secret store (Vault) or an S3 bucket and writes it to a shared volume that the main app reads.
1spec:2 volumes:3 - name: app-config4 emptyDir: {} # Temporary volume shared between init and main container5 6 initContainers:7 - name: init-fetch-config8 image: vault:1.159 command:10 - sh11 - -c12 - |13 vault kv get -field=config secret/payments-api/production \14 > /config/app.yaml15 echo "Config written to /config/app.yaml"16 volumeMounts:17 - name: app-config18 mountPath: /config19 env:20 - name: VAULT_ADDR21 value: "http://vault.internal:8200"22 - name: VAULT_TOKEN23 valueFrom:24 secretKeyRef:25 name: vault-token26 key: token27 28 containers:29 - name: payments-api30 image: registry.razorpay.in/payments-api:v2.5.131 volumeMounts:32 - name: app-config33 mountPath: /etc/app # App reads config from hereInit container: Main container: writes β /config/app.yaml reads β /etc/app/app.yaml (via shared emptyDir volume)Init Container Pattern 3 β Database Migration Before Deploy
The most critical pattern at companies like Zerodha or PhonePe. Schema migrations must complete before the new app version starts handling requests.
1initContainers:2 - name: init-db-migration3 image: registry.razorpay.in/payments-api:v2.5.1 # Same image as the app4 command: ["python", "manage.py", "migrate", "--no-input"]5 env:6 - name: DATABASE_URL7 valueFrom:8 secretKeyRef:9 name: db-credentials10 key: urlThis is simpler than a separate Job when you want the migration to be tightly coupled to each pod's lifecycle β though for large clusters, a separate Job (as covered in the Jobs topic) gives more control.
Sidecar Containers β Run Alongside the App Forever
A sidecar starts when the pod starts and runs for the entire pod lifetime alongside the main application. It handles a cross-cutting concern β logging, metrics, service mesh proxy β without the main app needing to know about it.
Timeline: Pod startsββββ [init-wait-for-db] runs β completesββββ [payments-api] starts ββββββββββββββββββββββββββββ runs foreverβββ [log-forwarder] starts ββββββββββββββββββββββββββββ runs foreverβββ [envoy-proxy] starts ββββββββββββββββββββββββββββ runs forever β Pod terminatesSidecar Pattern 1 β Log Forwarding
Your app writes logs to files on disk. A sidecar reads those files and ships them to a central system (Elasticsearch, Loki, Datadog).
1spec:2 volumes:3 - name: app-logs4 emptyDir: {}5 6 containers:7 - name: payments-api8 image: registry.razorpay.in/payments-api:v2.5.19 volumeMounts:10 - name: app-logs11 mountPath: /var/log/app # App writes logs here12 13 - name: log-forwarder14 image: fluent/fluent-bit:2.215 volumeMounts:16 - name: app-logs17 mountPath: /var/log/app # Sidecar reads from the same path18 env:19 - name: LOKI_URL20 value: "http://loki.monitoring:3100"payments-api β writes β /var/log/app/payments.log βlog-forwarder β reads β /var/log/app/payments.log β ships to LokiSidecar Pattern 2 β Service Mesh Proxy (Envoy / Istio)
In a service mesh, a sidecar proxy intercepts all network traffic to and from the pod. This is how Istio works β Envoy is automatically injected as a sidecar into every pod in a mesh-enabled namespace.
1# Istio injects this automatically β shown here for understanding2containers:3 - name: payments-api4 image: registry.razorpay.in/payments-api:v2.5.15 ports:6 - containerPort: 80807 8 - name: envoy-proxy # Injected by Istio automatically9 image: envoyproxy/envoy:v1.2810 ports:11 - containerPort: 15001 # Intercepts all outbound traffic12 - containerPort: 15006 # Intercepts all inbound trafficInbound request:Internet β Envoy (sidecar) β payments-api app β Enforces mTLS Records metrics Applies retry policy Outbound request:payments-api app β Envoy (sidecar) β payments-db β Applies circuit breaker Load balances Encrypts with mTLSSidecar Pattern 3 β Secrets Refresh Without Restart
A sidecar periodically fetches updated secrets from Vault and writes them to a shared volume. The main app reads secrets from files rather than environment variables β so it picks up rotated secrets without a pod restart.
1spec:2 volumes:3 - name: secrets-volume4 emptyDir:5 medium: Memory # Store secrets in memory, not on disk6 7 containers:8 - name: payments-api9 image: registry.razorpay.in/payments-api:v2.5.110 volumeMounts:11 - name: secrets-volume12 mountPath: /var/secrets13 readOnly: true14 15 - name: vault-agent16 image: vault:1.1517 command: ["vault", "agent", "-config=/etc/vault/config.hcl"]18 volumeMounts:19 - name: secrets-volume20 mountPath: /var/secrets # Writes refreshed secrets here every 15 minutesResource Allocation β Init vs Sidecar
Init containers are sequential β only one runs at a time. The pod's effective resource request for init containers is the maximum of any single init container, not the sum.
1Init containers:2 init-wait-db: cpu: 50m, memory: 64Mi3 init-migration: cpu: 200m, memory: 256Mi4 5Effective init resource request = max(50m, 200m) = 200m CPU, max(64Mi, 256Mi) = 256Mi6 7Regular containers (run simultaneously):8 payments-api: cpu: 500m, memory: 512Mi9 log-forwarder: cpu: 100m, memory: 128Mi10 11Effective regular resource request = sum = 600m CPU, 640Mi memory12 13Total pod resource request = max(init_max, regular_sum) per resource14= 600m CPU (regular wins), 640Mi memory (regular wins)Kubernetes 1.29+ Native Sidecar Containers
From Kubernetes 1.29, sidecars can be declared as initContainers with restartPolicy: Always. This is the new native sidecar gate β it starts before the main app but runs for the pod's lifetime.
1initContainers:2 - name: log-forwarder3 image: fluent/fluent-bit:2.24 restartPolicy: Always # Makes this a native sidecar (K8s 1.29+)5 # Starts before main containers, runs forever alongside them6 # Kubernetes waits for it to be Ready before starting main containers7 8containers:9 - name: payments-api10 image: registry.razorpay.in/payments-api:v2.5.1The advantage over the old pattern: Kubernetes now understands the startup ordering β the sidecar is fully up before the main app starts, and on shutdown, the main app terminates first, then the sidecar.
Debugging Init Containers
1# Pod stuck in Init:0/2 β read the first init container's logs2kubectl logs <pod-name> -c init-wait-for-postgres -n production3 4# If the init container has not started yet (CrashLoopBackOff)5kubectl logs <pod-name> -c init-wait-for-postgres -n production --previous6 7# Describe the pod for events8kubectl describe pod <pod-name> -n production9# Look for: "Back-off restarting failed init container"π΄ Common Mistake: Using an init container to wait for a service by hostname (e.g. postgres-service) without ensuring DNS is available in the init container. Use nslookup postgres-service in the init container to verify DNS resolves before attempting the TCP check.
π‘ Tip: At Swiggy or Hotstar, sidecar resource requests are often misconfigured β teams set zero requests on the log forwarder sidecar to "save resources." The result is the sidecar gets OOMKilled under load and stops shipping logs precisely when you need them most during an incident. Always set meaningful resource requests and limits on sidecar containers, not just the main app container.