Kubernetes Init Containers and Sidecar Patterns (2026)
Kubernetes pods are not always single-container affairs. Two powerful multi-container patterns — init containers and sidecars — let you cleanly separate concerns such as bootstrapping, dependency waiting, log shipping, and proxy injection from your main application logic. In 2026, Kubernetes 1.29+ formalized the native sidecar feature, making lifecycle management even more predictable. This guide covers both patterns end-to-end: the spec, real-world use cases with full YAML examples, the new native sidecar syntax, resource considerations, and troubleshooting.
Table of Contents
- Pod Lifecycle Recap: Init Containers First
- Init Container Spec Deep Dive
- Use Case 1: Wait-for-Service Init Container
- Use Case 2: Database Migration Init Container
- Use Case 3: Git-Clone Init Container
- Use Case 4: Fetching Secrets from Vault
- Sharing Data Between Init and App Containers
- The Sidecar Pattern Explained
- Sidecar Use Case 1: Fluentd Log Shipping
- Sidecar Use Case 2: Envoy / Istio Proxy
- Sidecar Use Case 3: OAuth2 Proxy Sidecar
- Native Sidecar Containers (K8s 1.29+)
- Resource Considerations
- Troubleshooting Init and Sidecar Containers
1. Pod Lifecycle Recap: Init Containers First
When the Kubernetes scheduler places a pod on a node, the kubelet starts containers in a strictly ordered sequence:
- Init containers run one at a time, in the order they are declared in
spec.initContainers. Each must exit with code0before the next starts. - Only after all init containers have succeeded does Kubernetes start the app containers in
spec.containers, which then run concurrently.
This sequencing guarantee is the key insight. You get a hook that fires before any application code runs and can block pod readiness until preconditions are satisfied — without baking that logic into your main image.
restartPolicy). With restartPolicy: Always on the pod, a failing init container retries indefinitely. With restartPolicy: Never the pod moves to Failed after the first init failure.
See Kubernetes Pods Guide for a complete breakdown of pod phases, conditions, and container states.
2. Init Container Spec Deep Dive
Init containers support almost everything a regular container supports — images, environment variables, volume mounts, resource limits, security contexts — but with two important differences:
- They do not support
lifecycle,livenessProbe,readinessProbe, orstartupProbehooks (these make no sense for a one-shot task). - Each init container must terminate before the next one starts. There is no concept of concurrent init containers (unless you use the native sidecar syntax introduced in 1.29).
A minimal pod with two init containers:
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
initContainers:
- name: check-config
image: busybox:1.36
command: ["sh", "-c", "test -f /config/app.yaml && echo 'Config OK'"]
volumeMounts:
- name: config-vol
mountPath: /config
- name: wait-db
image: busybox:1.36
command: ["sh", "-c",
"until nc -z postgres-svc 5432; do echo 'Waiting for DB...'; sleep 2; done"]
containers:
- name: app
image: my-app:2.1.0
ports:
- containerPort: 8080
volumeMounts:
- name: config-vol
mountPath: /config
volumes:
- name: config-vol
configMap:
name: app-config
Using a separate lightweight image (like busybox or alpine) for init containers is intentional — it keeps your app image lean and avoids bundling tooling that is only needed at startup.
3. Use Case 1: Wait-for-Service Init Container
One of the most common init container patterns is blocking the app until an upstream service — a database, a message broker, a gRPC service — is reachable. This prevents your application from starting and immediately crashing with a connection-refused error.
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
initContainers:
# Wait for PostgreSQL
- name: wait-for-postgres
image: busybox:1.36
command:
- sh
- -c
- |
echo "Waiting for PostgreSQL at postgres-svc:5432..."
until nc -z -w 2 postgres-svc 5432; do
echo "PostgreSQL not ready — sleeping 3s"
sleep 3
done
echo "PostgreSQL is ready!"
# Wait for Redis cache
- name: wait-for-redis
image: busybox:1.36
command:
- sh
- -c
- |
echo "Waiting for Redis at redis-svc:6379..."
until nc -z -w 2 redis-svc 6379; do
echo "Redis not ready — sleeping 3s"
sleep 3
done
echo "Redis is ready!"
containers:
- name: order-service
image: company/order-service:3.4.1
ports:
- containerPort: 8080
env:
- name: SPRING_DATASOURCE_URL
value: "jdbc:postgresql://postgres-svc:5432/orders"
- name: SPRING_REDIS_HOST
value: redis-svc
nc -z -w 2 (netcat with a 2-second timeout) over wget or curl for TCP-level checks. For HTTP health endpoints use wget -q --spider http://svc/health instead.
4. Use Case 2: Database Migration Init Container
Running schema migrations before your application starts is a classic twelve-factor app concern. Doing it in an init container means migrations run exactly once per pod start, are visible in pod events, and block the app from starting if they fail.
This example uses Flyway to run migrations against a PostgreSQL database:
apiVersion: v1
kind: Pod
metadata:
name: user-service-migrator
namespace: production
spec:
restartPolicy: OnFailure
initContainers:
- name: flyway-migrate
image: flyway/flyway:10.12-alpine
args:
- -url=jdbc:postgresql://postgres-svc:5432/users
- -user=$(DB_USER)
- -password=$(DB_PASSWORD)
- -locations=filesystem:/flyway/sql
- migrate
env:
- name: DB_USER
valueFrom:
secretKeyRef:
name: postgres-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-credentials
key: password
volumeMounts:
- name: migrations
mountPath: /flyway/sql
containers:
- name: user-service
image: company/user-service:5.0.0
ports:
- containerPort: 8080
env:
- name: DB_HOST
value: postgres-svc
- name: DB_NAME
value: users
- name: DB_USER
valueFrom:
secretKeyRef:
name: postgres-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-credentials
key: password
volumes:
- name: migrations
configMap:
name: flyway-sql-scripts
Learn more about managing configuration and secrets in Kubernetes ConfigMaps & Secrets.
5. Use Case 3: Git-Clone Init Container
When your application needs configuration files, HTML templates, or seed data that live in a Git repository, a git-clone init container can pull the latest commit into a shared emptyDir volume before the app starts. This keeps your app image free of repository credentials and always serves fresh content.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-static
namespace: web
spec:
replicas: 2
selector:
matchLabels:
app: nginx-static
template:
metadata:
labels:
app: nginx-static
spec:
initContainers:
- name: git-clone
image: alpine/git:2.43.0
command:
- sh
- -c
- |
git clone --depth 1 \
https://$(GIT_TOKEN)@github.com/company/website-content.git \
/data/html
echo "Cloned $(git -C /data/html rev-parse --short HEAD)"
env:
- name: GIT_TOKEN
valueFrom:
secretKeyRef:
name: github-token
key: token
volumeMounts:
- name: web-content
mountPath: /data
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
volumeMounts:
- name: web-content
mountPath: /usr/share/nginx/html
subPath: html
volumes:
- name: web-content
emptyDir: {}
6. Use Case 4: Fetching Secrets from Vault
HashiCorp Vault is a popular secrets backend. Rather than using the Vault Agent Injector (which is itself a sidecar), some teams prefer a simpler init container that writes secrets to a shared volume before the app starts. This avoids the Vault agent overhead and keeps the app image agnostic of Vault.
apiVersion: v1
kind: Pod
metadata:
name: payment-service
namespace: production
annotations:
vault.hashicorp.com/agent-inject: "false" # disable auto-injection if Vault webhook is installed
spec:
serviceAccountName: payment-service-sa
initContainers:
- name: vault-fetch-secrets
image: hashicorp/vault:1.16
command:
- sh
- -c
- |
# Authenticate using Kubernetes service-account token
VAULT_TOKEN=$(vault write -field=token auth/kubernetes/login \
role=payment-service \
jwt=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token))
export VAULT_TOKEN
# Fetch secrets and write to shared volume
vault kv get -field=api_key secret/payment/stripe > /secrets/stripe_api_key
vault kv get -field=db_pass secret/payment/postgres > /secrets/db_password
vault kv get -field=hmac_key secret/payment/signing > /secrets/hmac_key
echo "Secrets fetched successfully"
env:
- name: VAULT_ADDR
value: "https://vault.internal:8200"
- name: VAULT_CACERT
value: /vault-ca/ca.crt
volumeMounts:
- name: secrets-vol
mountPath: /secrets
- name: vault-ca
mountPath: /vault-ca
containers:
- name: payment-service
image: company/payment-service:4.2.0
ports:
- containerPort: 9090
env:
- name: STRIPE_API_KEY_FILE
value: /secrets/stripe_api_key
- name: DB_PASSWORD_FILE
value: /secrets/db_password
volumeMounts:
- name: secrets-vol
mountPath: /secrets
readOnly: true
volumes:
- name: secrets-vol
emptyDir:
medium: Memory # tmpfs — never written to disk
- name: vault-ca
configMap:
name: vault-ca-cert
emptyDir.medium: Memory mounts the volume as a tmpfs filesystem. Secrets live only in RAM and are never written to the node's disk, which reduces the blast radius of a node compromise.
7. Sharing Data Between Init and App Containers: emptyDir
The mechanism behind all of the above patterns is the emptyDir volume. It is created empty when a pod is scheduled to a node and deleted when the pod is removed. Both init containers and app containers can mount the same named volume, allowing data to flow from init phase to runtime.
spec:
volumes:
- name: shared-data # declared once at pod level
emptyDir: {}
initContainers:
- name: populate
image: busybox:1.36
command: ["sh", "-c", "echo 'bootstrap content' > /shared/init.txt"]
volumeMounts:
- name: shared-data
mountPath: /shared # init writes here
containers:
- name: app
image: my-app:latest
volumeMounts:
- name: shared-data
mountPath: /app/data # app reads from here
Key rules:
- The volume name must match in both
volumesand everyvolumeMountsentry that references it. - Files written by an init container persist in the
emptyDirfor the lifetime of the pod, even after the init container exits. - If the pod is evicted or rescheduled, the
emptyDircontent is lost and init containers re-run to repopulate it — which is the correct behavior.
For persistent data that must survive pod restarts, use a PersistentVolumeClaim. See Kubernetes Storage Guide for details.
8. The Sidecar Pattern Explained
A sidecar is a container that runs alongside your main application container in the same pod, sharing the pod's network namespace, process namespace (optionally), and any mounted volumes. Unlike init containers, sidecars run concurrently with the app and for the pod's entire lifetime.
When to Use a Sidecar vs. an Init Container
| Criterion | Init Container | Sidecar |
|---|---|---|
| Lifecycle | Runs once, must exit 0 | Runs for the pod's lifetime |
| Purpose | Bootstrap, dependency gating, one-time setup | Ongoing auxiliary services (logging, proxy, metrics) |
| Concurrency | Sequential with other init containers | Concurrent with main app and other sidecars |
| Restarts | Restarted if it fails (per restartPolicy) | Restarted if it fails (per restartPolicy) |
Common sidecar use cases: log collection and forwarding, service mesh proxies (Envoy/Istio), authentication proxies (OAuth2), metrics exporters, secret rotation agents, and TLS termination.
9. Sidecar Use Case 1: Fluentd Log Shipping
Applications that write logs to files (rather than stdout) need a mechanism to get those logs into a centralised store such as Elasticsearch or a cloud logging service. A Fluentd sidecar mounts the same log directory as the app, tails the files, and forwards records upstream — without any change to the app container image.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
# ── Main application ──────────────────────────────
- name: api-server
image: company/api-server:6.1.0
ports:
- containerPort: 8080
env:
- name: LOG_DIR
value: /var/log/app
volumeMounts:
- name: log-volume
mountPath: /var/log/app
# ── Fluentd sidecar ───────────────────────────────
- name: fluentd
image: fluent/fluentd-kubernetes-daemonset:v1.16-debian-elasticsearch8-1
env:
- name: FLUENT_ELASTICSEARCH_HOST
value: elasticsearch-svc
- name: FLUENT_ELASTICSEARCH_PORT
value: "9200"
- name: FLUENTD_SYSTEMD_CONF
value: disable
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 500m
memory: 500Mi
volumeMounts:
- name: log-volume
mountPath: /var/log/app
readOnly: true # sidecar only reads logs
- name: fluentd-config
mountPath: /fluentd/etc
volumes:
- name: log-volume
emptyDir: {}
- name: fluentd-config
configMap:
name: fluentd-config
/var/log/containers automatically.
10. Sidecar Use Case 2: Envoy / Istio Proxy Sidecar
Istio's service mesh injects an Envoy proxy sidecar into every pod automatically. The proxy intercepts all inbound and outbound traffic, enabling mTLS, traffic management, observability, and circuit breaking — all without changing application code.
Enabling Automatic Sidecar Injection
Label the namespace so Istio injects Envoy automatically:
kubectl label namespace production istio-injection=enabled
Every new pod in the production namespace now gets an istio-proxy sidecar and an istio-init init container (which configures iptables rules) injected automatically by the Istio mutating webhook.
Manually Adding an Envoy Sidecar (without Istio)
apiVersion: v1
kind: Pod
metadata:
name: grpc-service
namespace: production
spec:
containers:
# ── Main gRPC service ─────────────────────────────
- name: grpc-service
image: company/grpc-service:2.0.0
ports:
- containerPort: 50051
# ── Envoy proxy sidecar ───────────────────────────
- name: envoy
image: envoyproxy/envoy:v1.30-latest
ports:
- containerPort: 9901 # Envoy admin
- containerPort: 15001 # outbound traffic
args:
- -c /etc/envoy/envoy.yaml
- --service-cluster grpc-service
- --service-node grpc-service
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
volumeMounts:
- name: envoy-config
mountPath: /etc/envoy
volumes:
- name: envoy-config
configMap:
name: envoy-grpc-config
For a full walkthrough of Istio traffic management, retries, and circuit breaking, see Kubernetes Istio Service Mesh.
11. Sidecar Use Case 3: OAuth2 Proxy Sidecar for Authentication
The OAuth2 Proxy project provides a reverse-proxy sidecar that adds OIDC/OAuth2 authentication in front of any HTTP service. This pattern is particularly useful for internal dashboards (Grafana, Kibana, internal APIs) where you want SSO without modifying the application.
apiVersion: apps/v1
kind: Deployment
metadata:
name: internal-dashboard
namespace: tools
spec:
replicas: 1
selector:
matchLabels:
app: internal-dashboard
template:
metadata:
labels:
app: internal-dashboard
spec:
containers:
# ── Grafana (upstream service) ────────────────────
- name: grafana
image: grafana/grafana:10.4.0
ports:
- containerPort: 3000
env:
- name: GF_SERVER_ROOT_URL
value: "https://dashboard.internal.example.com"
- name: GF_AUTH_PROXY_ENABLED
value: "true"
- name: GF_AUTH_PROXY_HEADER_NAME
value: X-Auth-Request-Email
# ── OAuth2 Proxy sidecar ──────────────────────────
- name: oauth2-proxy
image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
ports:
- containerPort: 4180 # exposed to ingress
args:
- --provider=google
- --upstream=http://localhost:3000
- --http-address=0.0.0.0:4180
- --redirect-url=https://dashboard.internal.example.com/oauth2/callback
- --email-domain=company.com
- --cookie-secure=true
- --cookie-httponly=true
- --cookie-samesite=lax
- --set-xauthrequest=true
env:
- name: OAUTH2_PROXY_CLIENT_ID
valueFrom:
secretKeyRef:
name: oauth2-proxy-secret
key: client-id
- name: OAUTH2_PROXY_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: oauth2-proxy-secret
key: client-secret
- name: OAUTH2_PROXY_COOKIE_SECRET
valueFrom:
secretKeyRef:
name: oauth2-proxy-secret
key: cookie-secret
4180. The proxy validates the session cookie or redirects to Google for login. On success it forwards requests to localhost:3000 (Grafana) — possible because both containers share the pod's loopback network.
12. Native Sidecar Containers (K8s 1.29+)
Traditional sidecars placed in spec.containers have a lifecycle problem: there is no guarantee they start before the main app, and when a Job completes, a sidecar that tails logs keeps the pod alive indefinitely. Kubernetes 1.29 graduated the native sidecar feature (KEP-753), which solves these problems by placing long-lived sidecars inside spec.initContainers with restartPolicy: Always.
Native Sidecar YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-with-native-sidecar
namespace: production
spec:
replicas: 2
selector:
matchLabels:
app: app-native
template:
metadata:
labels:
app: app-native
spec:
initContainers:
# ── Traditional init container (runs once) ─────────
- name: wait-for-db
image: busybox:1.36
command: ["sh", "-c",
"until nc -z postgres-svc 5432; do sleep 2; done"]
# No restartPolicy — defaults to the pod's policy (runs once)
# ── Native sidecar (runs for pod lifetime) ─────────
- name: log-forwarder
image: fluent/fluent-bit:3.0
restartPolicy: Always # ← this is the key field
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
volumeMounts:
- name: log-vol
mountPath: /var/log/app
readOnly: true
- name: fluent-bit-config
mountPath: /fluent-bit/etc
containers:
- name: app
image: company/app:7.0.0
ports:
- containerPort: 8080
volumeMounts:
- name: log-vol
mountPath: /var/log/app
volumes:
- name: log-vol
emptyDir: {}
- name: fluent-bit-config
configMap:
name: fluent-bit-config
Benefits Over Traditional Sidecars
- Guaranteed startup order: Native sidecars start before any
spec.containerscontainer (they complete their startup probe or become running before the next initContainer / main container starts). - Clean Job termination: When all regular containers in a Job pod exit, Kubernetes sends SIGTERM to native sidecars — so log forwarders flush and exit cleanly without blocking pod completion.
- Automatic restart:
restartPolicy: AlwaysinsideinitContainersmeans the sidecar is restarted if it crashes, independent of the main app's restart. - Resource accounting: Native sidecars are correctly counted in pod resource calculations from the start.
SidecarContainers feature gate enabled (on by default from 1.29). Check your cluster version with kubectl version --short.
13. Resource Considerations
Init containers and sidecars both count toward the pod's effective resource footprint, but in different ways.
Init Container Resource Accounting
Because init containers run sequentially, the pod's effective resource request is the maximum of:
- The highest resource request across all init containers, OR
- The sum of all regular containers' resource requests
Whichever is larger wins. If your init containers are heavyweight (e.g., a Flyway container that needs 512 Mi to run migrations), that can inflate your pod's schedulable footprint even though that memory is only used for a few seconds.
# Pod with these containers:
# init-1: requests 1 CPU, 512Mi
# init-2: requests 500m CPU, 256Mi
# app: requests 500m CPU, 256Mi
#
# Effective pod request = max(max(init-1, init-2), sum(app))
# = max(max(1CPU/512Mi, 500m/256Mi), 500m/256Mi)
# = 1 CPU, 512Mi ← driven by init-1
Sidecar Resource Accounting
Traditional sidecars in spec.containers are summed with the main app — straightforward. Native sidecars in initContainers with restartPolicy: Always are included in the sum of spec.containers resources for scheduling purposes (as of K8s 1.29).
resources.requests and resources.limits on every container, including init and sidecar containers. Without limits, a runaway Fluentd sidecar can OOM-kill the node. See Complete K8s Guide for resource quota and LimitRange patterns.
14. Troubleshooting Init and Sidecar Containers
When a pod does not reach Running state, the first step is understanding which container is responsible.
Check Pod Status
# Overview — look at STATUS and READY columns
kubectl get pod my-app -n production
# Detailed events and container states
kubectl describe pod my-app -n production
Common init container statuses:
Init:0/2— zero of two init containers have completed.Init:Error— an init container exited with a non-zero code.Init:CrashLoopBackOff— an init container is crashing and being restarted with exponential backoff.PodInitializing— all init containers succeeded; app containers are starting.
View Init Container Logs
# Specify the init container with -c
kubectl logs my-app -n production -c wait-for-postgres
# Follow logs of a running init container
kubectl logs my-app -n production -c flyway-migrate -f
# Previous instance of a crashed init container
kubectl logs my-app -n production -c vault-fetch-secrets --previous
View Sidecar Container Logs
# Sidecar containers are in spec.containers — use -c with the container name
kubectl logs my-app -n production -c fluentd
kubectl logs my-app -n production -c envoy --tail=100
Exec Into a Running Container for Debugging
# Exec into the main app container
kubectl exec -it my-app -n production -- /bin/sh
# Exec into a named sidecar
kubectl exec -it my-app -n production -c fluentd -- /bin/sh
Common Problems and Fixes
| Symptom | Likely Cause | Fix |
|---|---|---|
Init container stuck in Init:0/1 forever |
Dependency (DB/service) never becomes reachable | Check network policy, service DNS, and that the dependency pod is Running |
Init:CrashLoopBackOff |
Init container exits non-zero | kubectl logs -c <name> --previous to see the error |
| App container starts but crashes immediately | Init container wrote incomplete/incorrect data to shared volume | Check init container logs; add ls -la /shared at end of init script |
| Job pod never completes | Traditional sidecar blocking pod exit | Migrate sidecar to native sidecar (restartPolicy: Always) or use a lifecycle preStop hook to stop the sidecar |
| Pod OOMKilled | Sidecar memory limit too low or no limit set | Add resources.limits.memory to the sidecar; increase if Fluentd/Envoy is buffering too much |
kubectl get events --field-selector involvedObject.name=my-app -n production --sort-by='.lastTimestamp' to see a chronological stream of events including image pull failures, OOMKills, and scheduling errors.
Conclusion
Init containers and sidecars are two of the most powerful composition tools in the Kubernetes toolkit. Init containers give you a clean, sequenced bootstrap phase — ideal for dependency gating, migrations, secret fetching, and volume pre-population. Sidecars give you a first-class way to attach long-running auxiliary processes (log shippers, proxies, authentication wrappers) to your app without altering its image. And with native sidecar containers landing in Kubernetes 1.29+, you now have an officially supported primitive that combines the best of both worlds: guaranteed startup order, correct Job lifecycle termination, and independent restart policies.
Master these patterns and you will write smaller, more focused container images, cleaner Helm charts, and pods that are far easier to troubleshoot in production.