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.
+++
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.
apiVersion: apps/v1kind: Deploymentmetadata: name: payments-api namespace: productionspec: replicas: 3 selector: matchLabels: app: payments-api template: spec: initContainers: - name: init-wait-for-postgres image: busybox:1.36 command: - sh - -c - | echo "Waiting for PostgreSQL to be ready..." until nc -z postgres-service 5432; do echo "PostgreSQL not ready — retrying in 5s" sleep 5 done echo "PostgreSQL is ready" - name: init-wait-for-redis image: busybox:1.36 command: - sh - -c - | until nc -z redis-service 6379; do sleep 2 done echo "Redis is ready" containers: - name: payments-api image: registry.razorpay.in/payments-api:v2.5.1 ports: - containerPort: 8080# Watch init containers progresskubectl get pod payments-api-7d9f8b-xk2p9 -n production -w # Output:# NAME READY STATUS RESTARTS AGE# payments-api-7d9f8b-xk2p9 0/1 Init:0/2 0 5s# payments-api-7d9f8b-xk2p9 0/1 Init:1/2 0 12s# payments-api-7d9f8b-xk2p9 0/1 PodInitializing 0 15s# payments-api-7d9f8b-xk2p9 1/1 Running 0 16s # Read init container logs specificallykubectl logs payments-api-7d9f8b-xk2p9 \ -c init-wait-for-postgres \ -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.
spec: volumes: - name: app-config emptyDir: {} # Temporary volume shared between init and main container initContainers: - name: init-fetch-config image: vault:1.15 command: - sh - -c - | vault kv get -field=config secret/payments-api/production \ > /config/app.yaml echo "Config written to /config/app.yaml" volumeMounts: - name: app-config mountPath: /config env: - name: VAULT_ADDR value: "http://vault.internal:8200" - name: VAULT_TOKEN valueFrom: secretKeyRef: name: vault-token key: token containers: - name: payments-api image: registry.razorpay.in/payments-api:v2.5.1 volumeMounts: - name: app-config 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.
initContainers: - name: init-db-migration image: registry.razorpay.in/payments-api:v2.5.1 # Same image as the app command: ["python", "manage.py", "migrate", "--no-input"] env: - name: DATABASE_URL valueFrom: secretKeyRef: name: db-credentials 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).
spec: volumes: - name: app-logs emptyDir: {} containers: - name: payments-api image: registry.razorpay.in/payments-api:v2.5.1 volumeMounts: - name: app-logs mountPath: /var/log/app # App writes logs here - name: log-forwarder image: fluent/fluent-bit:2.2 volumeMounts: - name: app-logs mountPath: /var/log/app # Sidecar reads from the same path env: - name: LOKI_URL 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.
# Istio injects this automatically — shown here for understandingcontainers: - name: payments-api image: registry.razorpay.in/payments-api:v2.5.1 ports: - containerPort: 8080 - name: envoy-proxy # Injected by Istio automatically image: envoyproxy/envoy:v1.28 ports: - containerPort: 15001 # Intercepts all outbound traffic - 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.
spec: volumes: - name: secrets-volume emptyDir: medium: Memory # Store secrets in memory, not on disk containers: - name: payments-api image: registry.razorpay.in/payments-api:v2.5.1 volumeMounts: - name: secrets-volume mountPath: /var/secrets readOnly: true - name: vault-agent image: vault:1.15 command: ["vault", "agent", "-config=/etc/vault/config.hcl"] volumeMounts: - name: secrets-volume 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.
Init containers: init-wait-db: cpu: 50m, memory: 64Mi init-migration: cpu: 200m, memory: 256Mi Effective init resource request = max(50m, 200m) = 200m CPU, max(64Mi, 256Mi) = 256Mi Regular containers (run simultaneously): payments-api: cpu: 500m, memory: 512Mi log-forwarder: cpu: 100m, memory: 128Mi Effective regular resource request = sum = 600m CPU, 640Mi memory Total pod resource request = max(init_max, regular_sum) per resource= 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.
initContainers: - name: log-forwarder image: fluent/fluent-bit:2.2 restartPolicy: Always # Makes this a native sidecar (K8s 1.29+) # Starts before main containers, runs forever alongside them # Kubernetes waits for it to be Ready before starting main containers containers: - name: payments-api 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
# Pod stuck in Init:0/2 — read the first init container's logskubectl logs <pod-name> -c init-wait-for-postgres -n production # If the init container has not started yet (CrashLoopBackOff)kubectl logs <pod-name> -c init-wait-for-postgres -n production --previous # Describe the pod for eventskubectl describe pod <pod-name> -n production# 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.