Kubernetes Pod Design Patterns: Sidecar, Ambassador, Adapter & More
Pods are the smallest deployable unit in Kubernetes, yet most production workloads pack more than one container into a single pod. Understanding the design patterns that govern multi-container pods is essential for building resilient, observable, and maintainable systems. This guide covers every major pod pattern, from sidecars to disruption budgets.
Why Multi-Container Pods?#
A pod shares a network namespace, IPC namespace, and optionally volumes across all its containers. This tight coupling is intentional — it lets helper containers augment a primary application without modifying its code.
┌─────────────────────────── Pod ───────────────────────────┐
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ App │ │ Sidecar │ │ Adapter │ │
│ │ Container │ │ Container │ │ Container │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └──────── Shared Network + Volumes ───┘ │
└───────────────────────────────────────────────────────────┘
Containers in a pod can communicate over localhost, read and write the same volume mounts, and coordinate through lifecycle hooks. This makes patterns like logging, proxying, and format conversion trivial.
The Sidecar Pattern#
The sidecar is the most common multi-container pattern. A helper container runs alongside the main application, extending its functionality without changing its code.
Typical Use Cases#
- Log shipping — a Fluentd or Filebeat sidecar tails log files written by the app and forwards them to a central store.
- Service mesh proxies — Envoy or Linkerd inject a sidecar that handles mTLS, retries, and circuit breaking.
- Configuration sync — a sidecar watches a ConfigMap or external config store and reloads the app when values change.
Example: Log Shipping Sidecar#
apiVersion: v1
kind: Pod
metadata:
name: app-with-log-shipper
spec:
containers:
- name: app
image: my-app:latest
volumeMounts:
- name: log-volume
mountPath: /var/log/app
- name: log-shipper
image: fluent/fluentd:latest
volumeMounts:
- name: log-volume
mountPath: /var/log/app
readOnly: true
volumes:
- name: log-volume
emptyDir: {}
Both containers mount the same emptyDir volume. The app writes logs; the sidecar reads and ships them. Neither container needs to know about the other's implementation details.
The Ambassador Pattern#
An ambassador container proxies network traffic from the main container to external services. The app connects to localhost, and the ambassador handles discovery, routing, or protocol translation.
When to Use Ambassadors#
- Connecting to a sharded database cluster — the ambassador routes queries to the correct shard.
- Abstracting away service discovery — the app calls
localhost:6379while the ambassador resolves the actual Redis endpoint. - Adding TLS termination to a legacy app that only speaks plain TCP.
containers:
- name: app
image: my-app:latest
env:
- name: DB_HOST
value: "localhost"
- name: DB_PORT
value: "5432"
- name: ambassador
image: db-proxy:latest
ports:
- containerPort: 5432
The app remains blissfully unaware of connection pooling, failover, or topology changes.
The Adapter Pattern#
An adapter container transforms the output of the main container into a format expected by an external system. This is the inverse of the ambassador: instead of translating outbound requests, it translates outbound data.
Common Adapter Scenarios#
- Metrics normalization — converting application-specific metrics into Prometheus exposition format.
- Log format conversion — transforming unstructured logs into JSON for a logging pipeline.
- Protocol bridging — exposing a gRPC service as REST for legacy consumers.
containers:
- name: app
image: legacy-app:latest
volumeMounts:
- name: metrics
mountPath: /tmp/metrics
- name: adapter
image: prometheus-adapter:latest
volumeMounts:
- name: metrics
mountPath: /tmp/metrics
readOnly: true
ports:
- containerPort: 9090
Init Containers#
Init containers run to completion before any regular container starts. They are perfect for one-time setup tasks.
Use Cases for Init Containers#
- Schema migrations — run
flyway migratebefore the app boots. - Dependency checks — wait for a database or message broker to become available.
- Secret fetching — pull secrets from Vault and write them to a shared volume.
- File permissions — set ownership on mounted volumes that require specific UIDs.
initContainers:
- name: wait-for-db
image: busybox:latest
command: ['sh', '-c', 'until nc -z db-service 5432; do sleep 2; done']
- name: run-migrations
image: flyway/flyway:latest
args: ['migrate']
env:
- name: FLYWAY_URL
value: "jdbc:postgresql://db-service:5432/mydb"
Init containers run sequentially. The first must succeed before the second starts. If any init container fails, Kubernetes restarts the pod according to its restartPolicy.
Shared Volumes#
Shared volumes are the backbone of inter-container communication within a pod. The most common volume type for this purpose is emptyDir.
Volume Types for Pod Communication#
| Volume Type | Lifetime | Use Case |
|---|---|---|
emptyDir | Pod lifetime | Scratch space, caches, inter-container data |
emptyDir with medium: Memory | Pod lifetime | High-speed temporary storage backed by RAM |
configMap | Until ConfigMap changes | Shared configuration files |
projected | Pod lifetime | Combining multiple sources into one mount |
Memory-Backed Shared Volume#
volumes:
- name: shared-cache
emptyDir:
medium: Memory
sizeLimit: 256Mi
This creates a tmpfs mount. Reads and writes are extremely fast, but the data counts against the container's memory limit.
Lifecycle Hooks#
Kubernetes provides two lifecycle hooks: postStart and preStop. These let containers perform actions at specific points in their lifecycle.
preStop Hook#
The preStop hook fires before a container receives SIGTERM. It is critical for graceful shutdowns.
containers:
- name: app
image: my-app:latest
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5 && /app/graceful-shutdown"]
The sleep 5 gives the endpoints controller time to remove the pod from the Service before the app stops accepting connections. This eliminates dropped requests during rolling updates.
postStart Hook#
The postStart hook runs immediately after a container is created — but there is no guarantee it executes before the container's entrypoint. Use it for fire-and-forget initialization like registering with a service directory.
Resource Limits and Requests#
Every production container should declare resource requests and limits. Without them, the scheduler makes suboptimal placement decisions and noisy neighbors starve other workloads.
Requests vs Limits#
- Requests — the guaranteed minimum. The scheduler uses this to place the pod on a node with sufficient capacity.
- Limits — the hard ceiling. The container is throttled (CPU) or OOM-killed (memory) if it exceeds this.
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "512Mi"
Quality of Service Classes#
Kubernetes assigns a QoS class based on resource configuration:
- Guaranteed — requests equal limits for all containers. Last to be evicted.
- Burstable — at least one container has requests less than limits. Evicted after BestEffort pods.
- BestEffort — no requests or limits set. First to be evicted under memory pressure.
For critical workloads, always aim for Guaranteed QoS by setting requests equal to limits.
Pod Disruption Budgets#
A PodDisruptionBudget (PDB) limits how many pods in a set can be voluntarily disrupted at the same time. This is essential during node drains, cluster upgrades, and autoscaler scale-downs.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: my-app
Key Parameters#
minAvailable— the minimum number of pods that must remain running. Can be an integer or percentage.maxUnavailable— the maximum number of pods that can be unavailable. Inverse ofminAvailable.
Choose one, not both. A PDB with minAvailable: "50%" on a 4-replica Deployment ensures at least 2 pods survive any voluntary disruption.
PDB + Rolling Updates#
During a rolling update, the Deployment controller respects the PDB. If draining a node would violate the budget, the drain blocks until enough replacement pods are ready. This prevents cascading outages during maintenance windows.
Putting It All Together#
Here is a production-ready pod spec combining several patterns:
apiVersion: v1
kind: Pod
metadata:
name: production-app
labels:
app: my-app
spec:
initContainers:
- name: wait-for-deps
image: busybox:latest
command: ['sh', '-c', 'until nc -z redis-svc 6379; do sleep 1; done']
containers:
- name: app
image: my-app:latest
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "500m"
memory: "512Mi"
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
volumeMounts:
- name: logs
mountPath: /var/log/app
- name: log-shipper
image: fluent/fluentd:latest
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "100m"
memory: "128Mi"
volumeMounts:
- name: logs
mountPath: /var/log/app
readOnly: true
volumes:
- name: logs
emptyDir: {}
This spec waits for Redis, runs the app with Guaranteed QoS, ships logs via a sidecar, and shuts down gracefully with a preStop hook.
Key Takeaways#
- Use the sidecar pattern for cross-cutting concerns like logging, monitoring, and service mesh proxies.
- Use the ambassador pattern to abstract away external service complexity from your application.
- Use the adapter pattern to normalize output formats for downstream systems.
- Run one-time setup tasks in init containers — they guarantee sequential, pre-boot execution.
- Always set resource requests and limits to ensure predictable scheduling and avoid noisy-neighbor issues.
- Protect availability during maintenance with pod disruption budgets.
- Leverage lifecycle hooks for graceful startup and shutdown coordination.
Explore more system design guides at codelit.io/blog.
Try these templates
Continue learning
Go deeper on system design
As an Amazon Associate I earn from qualifying purchases. Codelit may receive a commission at no extra cost to you.
Building Microservices
Sam Newman · 2021
600 pages on splitting a monolith: service boundaries, data decomposition, migration order.
Release It!
Michael T. Nygard · 2018
Post-mortems of real outages, then the stability patterns (circuit breaker, bulkhead) that stop them.
4.6 (501)#4 in Client-Server Networking SystemsSite Reliability Engineering
Betsy Beyer, Niall Richard Murphy · 2016
Google's on-call postmortems, error-budget math, and staffing models across 550 pages of practice.
4.7 (1.2k)#1 in Network Disaster & Recovery AdministrationKubernetes: Up and Running
Brendan Burns, Joe Beda · 2022
Builds from a single Pod to multi-cluster deployments with YAML you type, not diagrams you skim.
4.6 (118)#10 in Web Services