Multi-Container Pod
Write a multi-container pod spec in Kubernetes: learn the sync pattern, sidecar design, shared volumes, and inter-container communication with a hands-on exercise. Includes troubleshooting and edge cases.
Focus: write a multi-container pod spec
So you've mastered single-container Pods — they deploy, they run, they die. But real-world applications rarely live in isolation. A web server needs a log shipper tailing its files; a proxy needs to reload config from a sidecar that watches a ConfigMap; a batch job needs a helper container to stage data before the main process starts. Stitching these containers together with kubectl and Deployments separately is messy, fragile, and violates the atomic unit of scheduling Kubernetes gives you. The pain is real, and the solution is the multi-container Pod spec — one YAML, two (or more) containers, zero wasted coupling.
The problem this lesson solves
When you put two containers into one Pod, you're not just saving lines of YAML. You're solving the co-scheduling problem: two processes that must run on the same node, share the same network namespace, and access the same filesystem. Without this, you resort to:
* Duplicate Deployments with podAntiAffinity hacks to force colocation — brittle and unpredictable.
* Sidecars deployed as separate Pods that communicate over a Service, adding latency and operational debt.
* emptyDir volumes shared via NFS or PVC mounts, which are overkill for transient data.
A multi-container Pod is the Kubernetes primitive that says: "these containers are born together, live together, and die together." Once you understand the spec, you unlock patterns — not workarounds.
Core concept / mental model
Think of a Pod as a shared envelope. Inside that envelope, every container:
- Shares the Pod IP (no Service needed for local communication)
- Shares the loopback interface (localhost resolves to the Pod IP)
- Mounts the same volumes (defined at Pod level, not per-container)
- Can communicate over inter-process communication (IPC) if shareProcessNamespace: true is set
Pro tip: Containers in the same Pod do not share port space — each container has its own network stack, but they both use the same IP. Port conflicts occur if two containers try to bind the same port inside the Pod. Use container-level
containerPortfields to document intent, not enforce exclusivity.
The mental model to hold onto: a Pod is a single unit of scheduling, but a multi-tenant runtime.
The three common patterns
| Pattern | Description | Use case |
|---|---|---|
| Sidecar | A helper container that extends the functionality of the main container (e.g., log shipper, proxy, config reloader) | nginx + fluentd tailing logs |
| Ambassador | A container that acts as a proxy or adapter for the main container to an external service | app + redis-client-sidecar |
| Adapter | A container that transforms the output or format of the main container (e.g., metrics format converter) | app + prometheus-adapter |
How it works step by step
- Define shared volumes at Pod level — every block in
spec.volumesis available to all containers. - List each container inside
spec.containers[]— each has its ownimage,command,ports,env,resources, andvolumeMounts. - Mount the same volume in different containers using unique
mountPathvalues (can be same path, but beware of file conflicts). - Optional: Enable process namespace sharing with
shareProcessNamespace: trueso containers can see each other's processes via/proc. - Container startup order is not guaranteed — Kubernetes starts them all simultaneously. Use
readinessProbesor wait-for scripts in an init container if sequencing is required.
Pro tip: For startup ordering, use an init container (not a regular container) to block until a condition is met, then let the main containers start.
Key YAML structure
apiVersion: v1
kind: Pod
metadata:
name: multi-container-demo
spec:
shareProcessNamespace: true
volumes:
- name: shared-data
emptyDir: {}
containers:
- name: main-app
image: nginx:latest
volumeMounts:
- name: shared-data
mountPath: /usr/share/nginx/html
ports:
- containerPort: 80
- name: log-sidecar
image: busybox
command: ["/bin/sh", "-c", "while true; do echo $(date) >> /logs/access.log; sleep 5; done"]
volumeMounts:
- name: shared-data
mountPath: /logs
Hands-on walkthrough
Let's build a practical multi-container Pod: a web server that writes files to a shared volume, and a sidecar that monitors those files and logs changes.
Step 1: Create the Pod YAML
Save this as multi-pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: monitor-pod
spec:
containers:
- name: writer
image: busybox
command: ["/bin/sh", "-c"]
args:
- |
while true; do
echo "data at $(date)" >> /var/shared/output.txt
sleep 3
done
volumeMounts:
- name: workdir
mountPath: /var/shared
- name: reader
image: busybox
command: ["/bin/sh", "-c"]
args:
- |
tail -f /var/shared/output.txt
volumeMounts:
- name: workdir
mountPath: /var/shared
volumes:
- name: workdir
emptyDir: {}
Step 2: Deploy and verify
kubectl apply -f multi-pod.yaml
kubectl get pods monitor-pod
Wait for Running status, then peek inside both containers:
# Check reader sidecar logs (it's tailing the file)
kubectl logs monitor-pod -c reader
# Sample output:
# data at Mon Mar 10 12:30:01 UTC 2025
# data at Mon Mar 10 12:30:04 UTC 2025
# Check writer container — same file, different mount path (no output, it just writes)
kubectl exec monitor-pod -c writer -- cat /var/shared/output.txt
# Should show same lines as reader log
Step 3: Inter-container communication via localhost
Add a third container that serves a web page from the shared volume, and another that curl's it.
apiVersion: v1
kind: Pod
metadata:
name: web-pod
spec:
containers:
- name: web
image: nginx:alpine
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
- name: curator
image: curlimages/curl:7.88.1
command: ["/bin/sh", "-c"]
args:
- |
while true; do
curl -s localhost:80/index.html
sleep 10
done
volumeMounts:
- name: html
mountPath: /html
volumes:
- name: html
emptyDir: {}
kubectl apply -f web-pod.yaml
kubectl logs web-pod -c curator
# Expected: HTML served by nginx (default welcome page)
Compare options / when to choose what
| Approach | Pros | Cons | Use when |
|---|---|---|---|
| Multi-container Pod | Co-located, shared network/volumes, atomic lifecycle | No horizontal scaling per container; restart policy shared | Sidecar, ambassador, adapter patterns |
| Two separate Deployments + Service | Independent scaling, separate lifecycle, different resource profiles | Network round trip, Service discovery overhead, more YAML | Microservices that need independent scaling |
| Init container + single main container | Sequential startup, no process namespace pollution | Only one workload type; no runtime cooperation | Setup tasks (wait for DB, seed data) |
| Job + CronJob | Batch execution, completions tracking | Not long-lived; no persistent shared volumes | One-shot sidecar tasks (rotate logs, backup) |
When to choose a multi-container Pod: You need two processes that:
- Must run on the same node (co-scheduling)
- Share ephemeral data via emptyDir
- Communicate via localhost for low latency
- Have the same lifecycle (restart together)
Troubleshooting & edge cases
"CrashLoopBackOff" on one container, other runs fine
Check the logs of the failing container. The stable container will keep running, but the Pod will be in a "not ready" state if the failing container lacks a readinessProbe. Solution: add a readinessProbe to containers that are allowed to fail independently (rare).
Port conflict even though they're in the same Pod
Each container has its own network stack, but they share the Pod IP. Two containers cannot both listen on the same port within the Pod. For example, nginx using port 80 and another nginx sidecar also on port 80 will fail → Address already in use. Use different port numbers or use the second container to connect externally (not listen on the same port).
Volume file permissions / ownership
Busybox containers often run as root; nginx runs as nginx (UID 101). Write from busybox and read from nginx → permission denied. Solution: set securityContext.fsGroup at Pod level so that written files have group ownership readable by both containers.
spec:
securityContext:
fsGroup: 101 # match nginx's group
Container fails and Pod restarts — lose data?
Yes, emptyDir volumes are ephemeral — they are deleted when the Pod disappears. If the main container crashes and the Pod restarts (based on restartPolicy: Always), the volume is new. For persistent data between crashes, use hostPath (node-local) or PVC.
What you learned & what's next
You now know: - How to write a multi-container pod spec with YAML, including shared volumes and inter-container communication. - The mental model of a Pod as a shared envelope (network, volumes, lifecycle). - The three common patterns: sidecar, ambassador, adapter. - When to use a multi-container Pod vs separate Deployments. - How to troubleshoot port conflicts, volume permissions, and crash loops.
Next up: Dive into the init container pattern, where you can delay container startup until dependencies are met — perfect for waiting on databases or pre-staging data before the main app launches.
Practice recap
Mini-exercise: Write a multi-container Pod spec that runs a Python web app on port 5000 (image python:3.10-slim, serving a simple HTTP server) and a sidecar that tail -f the app's logs (mounted via shared emptyDir). Deploy it with kubectl apply, then exec into the sidecar to verify it sees the log output. Adjust the sidecar's command to filter only ERROR-level logs.
Common mistakes
- Both containers try to bind the same port (e.g., port 80) — each container has its own network stack but they share the Pod IP; port conflict causes
CrashLoopBackOff. - Forgetting that
emptyDiris ephemeral — data is lost when the Pod restarts, not just when the container restarts. - Assuming containers start in the order they are defined — they start simultaneously; use init containers for sequencing.
- Not setting
securityContext.fsGroupwhen a non-root container reads files written by a root container results inPermission denied.
Variations
- Use
hostPathvolume instead ofemptyDirfor data that must survive Pod restarts, but be aware of node-binding. - Enable
shareProcessNamespace: truefor containers that need to signal each other (e.g., via SIGTERM) or access/proc. - Use a DaemonSet for sidecars that must run on every node, rather than bundling inside every Pod.
Real-world use cases
- A web server (nginx) container paired with a log shipper (fluentd) sidecar that tails access logs from a shared
emptyDirvolume. - A main application container that needs a Redis cache sidecar on the same localhost for ultra-low-latency lookups, without network overhead.
- A configuration reloader sidecar that watches a ConfigMap and signals the main nginx container via a shared process namespace to reload without restarting the Pod.
Key takeaways
- A multi-container Pod is a co-scheduled set of containers that share network (localhost), volumes (Pod-level), and lifecycle.
- Use
spec.volumesto define shared storage once; each container mounts viavolumeMountsat its own path. - Containers start simultaneously — use init containers or readiness probes for ordering dependencies.
- The three canonical patterns are sidecar, ambassador, and adapter — each solves a different tight-coupling problem.
- Port conflicts happen when two containers bind the same port inside the Pod — use different ports or talk via localhost on different ports.
- Always set
securityContext.fsGroupwhen sharing files between containers with different run-as users.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.