Kubernetes Multi-Container Pod Patterns
Implement multi-container pod patterns in Kubernetes: sidecar for logging and proxying, ambassador for outbound connections, adapter for format
π‘ Quick Answer: Multi-container pods share network (localhost) and storage (volumes). Three patterns: Sidecar β extends/enhances main container (log shipper, proxy, config reloader). Ambassador β proxies outbound connections (connection pooling, service discovery). Adapter β transforms output format (metrics exporter, log formatter). Containers in a pod always co-schedule and co-locate.
The Problem
- Need to add logging/monitoring to apps without changing application code
- Want to proxy connections to external services with retry/circuit-breaking
- Need to transform metrics format from application-specific to Prometheus
- Config files need live reloading without application restart
- Want separation of concerns between application logic and infrastructure
The Solution
Sidecar Pattern: Log Shipper
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
template:
spec:
containers:
# Main application
- name: app
image: registry.example.com/web-app:v2
ports:
- containerPort: 8080
volumeMounts:
- name: logs
mountPath: /var/log/app
# Sidecar: ships logs to central system
- name: log-shipper
image: fluent/fluent-bit:3.0
volumeMounts:
- name: logs
mountPath: /var/log/app
readOnly: true
- name: fluent-config
mountPath: /fluent-bit/etc/
resources:
requests:
cpu: "50m"
memory: "64Mi"
volumes:
- name: logs
emptyDir: {}
- name: fluent-config
configMap:
name: fluent-bit-configSidecar Pattern: Config Reloader
spec:
containers:
- name: nginx
image: nginx:1.25
volumeMounts:
- name: config
mountPath: /etc/nginx/conf.d
# Sidecar: watches ConfigMap and reloads nginx
- name: config-reloader
image: jimmidyson/configmap-reload:v0.12.0
args:
- --volume-dir=/config
- --webhook-url=http://localhost:80/-/reload
volumeMounts:
- name: config
mountPath: /config
readOnly: true
volumes:
- name: config
configMap:
name: nginx-configSidecar Pattern: TLS Proxy (Native Sidecar K8s 1.29+)
spec:
# Native sidecar container (K8s 1.29+) β starts before, stops after main
initContainers:
- name: tls-proxy
image: envoyproxy/envoy:v1.30
restartPolicy: Always # Makes it a native sidecar (stays running)
ports:
- containerPort: 8443
volumeMounts:
- name: certs
mountPath: /etc/certs
readOnly: true
containers:
- name: app
image: registry.example.com/app:v2
# App listens on localhost:8080 (plain HTTP)
# Envoy sidecar terminates TLS and forwards to app
env:
- name: PORT
value: "8080"Ambassador Pattern: Connection Pooling
spec:
containers:
- name: app
image: registry.example.com/app:v2
env:
# App connects to localhost β ambassador handles real connection
- name: DATABASE_HOST
value: "localhost"
- name: DATABASE_PORT
value: "5432"
# Ambassador: connection pooler for database
- name: pgbouncer
image: bitnami/pgbouncer:1.22
ports:
- containerPort: 5432
env:
- name: POSTGRESQL_HOST
value: "postgres.production.svc"
- name: POSTGRESQL_PORT
value: "5432"
- name: PGBOUNCER_POOL_MODE
value: "transaction"
- name: PGBOUNCER_MAX_CLIENT_CONN
value: "100"
- name: PGBOUNCER_DEFAULT_POOL_SIZE
value: "20"
resources:
requests:
cpu: "50m"
memory: "64Mi"Ambassador Pattern: Rate-Limited API Client
spec:
containers:
- name: app
image: registry.example.com/app:v2
env:
- name: API_ENDPOINT
value: "http://localhost:9090/api" # Via ambassador
# Ambassador: rate limiter + retry for external API
- name: api-proxy
image: envoyproxy/envoy:v1.30
ports:
- containerPort: 9090
volumeMounts:
- name: envoy-config
mountPath: /etc/envoy
# Envoy config: rate limit + circuit breaker + retry to external APIAdapter Pattern: Prometheus Metrics Exporter
spec:
containers:
- name: app
image: registry.example.com/legacy-app:v1
# App exposes custom /stats endpoint (not Prometheus format)
# Adapter: converts app metrics to Prometheus format
- name: metrics-adapter
image: registry.example.com/stats-exporter:v1
ports:
- containerPort: 9090
name: metrics
args:
- --source=http://localhost:8080/stats
- --format=prometheus
- --listen=:9090
resources:
requests:
cpu: "25m"
memory: "32Mi"Adapter Pattern: Log Format Transformer
spec:
containers:
- name: app
image: registry.example.com/legacy-app:v1
# App writes logs in custom format to shared volume
volumeMounts:
- name: logs
mountPath: /var/log/app
# Adapter: transforms log format and writes to stdout (for K8s log collection)
- name: log-adapter
image: busybox:1.36
command:
- sh
- -c
- |
tail -F /var/log/app/app.log | while read line; do
echo "{\"timestamp\":\"$(date -Iseconds)\",\"message\":\"$line\",\"app\":\"legacy-app\"}"
done
volumeMounts:
- name: logs
mountPath: /var/log/app
readOnly: true
volumes:
- name: logs
emptyDir: {}Pattern Comparison
Pattern β Direction β Purpose β Example
βββββββββββββΌβββββββββββββββΌββββββββββββββββββββββββββββββΌβββββββββββββββββ
Sidecar β Alongside β Enhance main container β Log shipper, proxy
Ambassador β Outbound β Proxy external connections β Connection pool
Adapter β Transform β Convert output format β Metrics exporter
βββββββββββββ΄βββββββββββββββ΄ββββββββββββββββββββββββββββββ΄βββββββββββββββββ
All patterns share:
- Same network namespace (localhost communication)
- Same volumes (shared filesystem)
- Same lifecycle (co-scheduled, co-located)
- Independent images and resource limitsCommon Issues
Sidecar starting after main container (race condition)
- Cause: Standard containers start simultaneously β no ordering guarantee
- Fix: Use native sidecar (K8s 1.29+,
restartPolicy: Alwaysin initContainers); or add readiness check
Main container exiting but sidecar keeps pod alive
- Cause: Pod only terminates when ALL containers exit
- Fix: Native sidecars (1.29+) shut down after main; or add lifecycle hook to signal sidecar
Resource limits not accounting for sidecars
- Cause: Total pod resources = sum of all containers; quotas apply to pod total
- Fix: Account for sidecar resources in capacity planning; set appropriate limits on sidecars
Best Practices
- Keep sidecars lightweight β they run on every pod instance; minimize CPU/memory
- Use native sidecars (1.29+) β guaranteed startup order and proper shutdown
- Share data via emptyDir β fast, no persistence needed for temp data
- Communicate via localhost β same network namespace, no service discovery needed
- Separate resource limits β sidecar shouldnβt compete with main container
- One responsibility per container β separation of concerns
- Consider service mesh instead β Istio/Linkerd automate sidecar proxy injection
Key Takeaways
- Multi-container pods share: network (localhost), storage (volumes), lifecycle
- Sidecar: enhances/extends (logging, proxying, config reload)
- Ambassador: proxies outbound connections (pooling, rate limiting, circuit breaking)
- Adapter: transforms output (metrics format, log structure, data conversion)
- Native sidecars (K8s 1.29+):
restartPolicy: Alwaysin initContainers β proper ordering - Containers communicate via localhost β no networking overhead
- Service meshes (Istio, Linkerd) are automated sidecar implementations

Recommended
Kubernetes Recipes β The Complete Book100+ production-ready patterns with detailed explanations, best practices, and copy-paste YAML. Everything in one place.
Get the Book βLearn by Doing
CopyPasteLearn β Hands-on Cloud & DevOps CoursesMaster Kubernetes, Ansible, Terraform, and MLOps with interactive, copy-paste-run lessons. Start free.
Browse Courses βπ Deepen Your Skills β Hands-on Courses
Courses by CopyPasteLearn.com β Learn IT by Doing
