Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3.3 Multi-Container Pods: Sidecars, Init, and Ambassadors

⏱️ 5 min read · 8 min hands-on · 🟢 Beginner

TL;DR: Multi-container pods solve specific co-location problems. The three patterns are: sidecar (augments the main container), init container (runs setup before the main container), and ambassador (proxies external traffic). Show the YAML first, understand the pattern second.

After this section you will be able to:

  • Differentiate between Init Containers and Sidecar Containers
  • Implement sidecar patterns for log aggregation, proxies, and configuration syncing
  • Coordinate initialization sequences where init containers complete before the main app boots

Pattern 1: Sidecar

A sidecar runs alongside the main container, sharing its network and storage. The most common use case: log shipping.

# sidecar-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: app-with-logger
spec:
  volumes:
  - name: shared-logs      # Shared volume between containers
    emptyDir: {}

  containers:
  # Main application
  - name: app
    image: nginx:1.25
    volumeMounts:
    - name: shared-logs
      mountPath: /var/log/nginx

  # Sidecar: ships logs from shared volume
  - name: log-shipper
    image: busybox
    command: ["sh", "-c", "tail -f /logs/access.log"]
    volumeMounts:
    - name: shared-logs
      mountPath: /logs         # Same volume, different mount path
graph LR
    subgraph "Pod: app-with-logger"
        APP[nginx<br/>writes to<br/>/var/log/nginx]
        VOL[(emptyDir<br/>shared-logs)]
        LOG[log-shipper<br/>reads from<br/>/logs]
        APP -->|writes| VOL
        VOL -->|reads| LOG
        LOG -->|ships to| EXT[External<br/>Log System]
    end

Key facts about emptyDir:

  • Created when the pod starts, deleted when the pod dies
  • Lives on the node’s disk (or RAM if medium: Memory)
  • Perfect for sharing data between containers in a pod

🔗 Docker Parallel: No direct equivalent in Compose. You’d need to set up a log driver or bind mount. The sidecar pattern keeps the main app container simple and logging as a separate concern.


Pattern 2: Init Containers

Init containers run sequentially before any regular containers start. They must complete successfully. If they fail, the pod restarts.

# init-container-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: app-with-init
spec:
  initContainers:
  # Runs FIRST — waits for a dependency to be ready
  - name: wait-for-db
    image: busybox
    command:
    - sh
    - -c
    - |
      until nc -z postgres-svc 5432; do
        echo "Waiting for postgres..."
        sleep 2
      done
      echo "Postgres is up!"

  # Runs SECOND — database migration
  - name: run-migrations
    image: myapp:latest
    command: ["python", "manage.py", "migrate"]
    env:
    - name: DATABASE_URL
      value: "postgresql://postgres-svc:5432/mydb"

  # Only starts AFTER all init containers succeed
  containers:
  - name: app
    image: myapp:latest
    ports:
    - containerPort: 8000
sequenceDiagram
    participant K as Kubernetes
    participant I1 as init: wait-for-db
    participant I2 as init: run-migrations
    participant A as app container

    K->>I1: Start
    I1-->>K: ✅ Exit 0
    K->>I2: Start
    I2-->>K: ✅ Exit 0
    K->>A: Start (finally!)
    A-->>K: Running

Why use init containers instead of a startup script in the main container?

  • Separation of concerns — the app image stays clean
  • Different image per init step (wait with busybox, migrate with your app)
  • Init containers can have different security contexts
  • Failures are clearly visible: kubectl get pod shows Init:0/2 then Init:1/2 etc.
# See init container progress
kubectl get pod app-with-init
# Output: Init:1/2  ← first init done, waiting on second

Pattern 3: Ambassador

An ambassador container proxies connections between the main container and external services — useful for handling TLS, service discovery, or protocol translation locally.

# ambassador-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: app-with-ambassador
spec:
  containers:
  # Main app talks to localhost:6379 (simple, no auth)
  - name: app
    image: myapp:latest
    env:
    - name: REDIS_HOST
      value: localhost      # ← talks to ambassador, not Redis directly
    - name: REDIS_PORT
      value: "6379"

  # Ambassador: handles auth and TLS to real Redis
  - name: redis-proxy
    image: envoyproxy/envoy:v1.29
    # Envoy configured to proxy localhost:6379 → redis-cloud.example.com:6380 (TLS+auth)

📝 Note: The ambassador pattern is less common today — service meshes (Istio, Linkerd) have largely replaced it for production TLS/auth proxying. But it’s still useful for local development and specific edge cases.


Checking Multi-Container Pods

With multiple containers, you must specify which one you’re targeting:

# Logs from a specific container
kubectl logs app-with-logger -c log-shipper
kubectl logs app-with-logger -c app

# Exec into a specific container
kubectl exec -it app-with-logger -c app -- bash
kubectl exec -it app-with-logger -c log-shipper -- sh

# Describe shows all containers
kubectl describe pod app-with-logger

Try It

# Deploy the sidecar pod
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: sidecar-demo
spec:
  volumes:
  - name: shared-data
    emptyDir: {}
  containers:
  - name: writer
    image: busybox
    command: ["sh", "-c", "while true; do date >> /data/log.txt; sleep 2; done"]
    volumeMounts:
    - name: shared-data
      mountPath: /data
  - name: reader
    image: busybox
    command: ["sh", "-c", "tail -f /shared/log.txt"]
    volumeMounts:
    - name: shared-data
      mountPath: /shared
EOF

# Watch both containers start
kubectl get pod sidecar-demo -w

# See the writer's output via reader's logs
kubectl logs sidecar-demo -c reader -f &

# Let it run for 10 seconds, then stop
sleep 10 && kill %1

# Exec into the writer and check the file directly
kubectl exec -it sidecar-demo -c writer -- cat /data/log.txt

# Cleanup
kubectl delete pod sidecar-demo

Expected log output (reader):

Mon Jul 14 10:00:01 UTC 2026
Mon Jul 14 10:00:03 UTC 2026
Mon Jul 14 10:00:05 UTC 2026

Key Takeaways

#PatternWhen to Use
1SidecarAugment main container: log shipping, metrics, proxy
2Init ContainerPre-flight checks, DB migrations, dependency waiting
3AmbassadorProtocol translation, local proxy to external service
4emptyDirEphemeral shared storage within a pod

✅ Quick Check

Q1: Your app container takes 60 seconds to initialize and fails readiness checks during that time. Should you use an init container or a startup probe?

Answer A **startup probe** (covered in Chapter 11). Init containers are for pre-flight tasks that run *before* the main container starts. If your app is initializing but already running, a startup probe tells K8s to give it more time before declaring it failed. Use init containers for things like DB migrations or waiting for dependencies.

Q2: An init container in a pod exits with code 1. What happens?

Answer The main containers never start. Kubernetes restarts the init container according to the pod's `restartPolicy`. If `restartPolicy: Always`, it keeps retrying with exponential back-off until the init container succeeds. The pod stays in `Init:0/N` status. This is by design — it prevents your app from starting against an unready dependency.

Q3: You have a sidecar container that ships logs to an external service. The main app container crashes and restarts. What happens to the sidecar?

Answer The sidecar keeps running. Container restarts in a pod are per-container — if only the main container crashes, only that container is restarted. The sidecar stays alive and continues running. (Exception: if the pod itself is killed and rescheduled, all containers restart together.)