Single & Multi Container #
One of the most important early architectural decisions when moving applications to Kubernetes is determining how to wrap our containers into Pods. By default, most Pods in Kubernetes contain just a single container (single-container Pod), and this is the standard pattern most recommended for the majority of workload scenarios.
However, Kubernetes is designed to support placing multiple containers simultaneously inside one Pod (multi-container Pod). This pattern is very useful when implemented with proper understanding, but it can become a performance disaster if abused to merge components that should be separate. Understanding the architectural boundaries between single and multi-container models helps us build scalable, maintainable clusters while avoiding unnecessary network and storage overhead.
The Basic Principle: The Pod as a Single Deployment Unit #
Before going further into the technical implementation, there’s one basic principle we must always hold tight:
All containers in one Pod are a single lifecycle and deployment unit.
That means those containers:
- Are always scheduled together to the same Worker Node by the Scheduler.
- Always start up and terminate together as one unit.
- Cannot be scaled independently. If we scale a Deployment to 5 replicas, Kubernetes duplicates the entire Pod contents (including the main container and its helper containers) into 5 pairs.
Here’s a guiding question to help us make decisions:
Must component A and component B always run on the same host server?
├── NO ➔ Split them into different Pods and Deployments (e.g. Frontend and Backend).
└── YES
└── Must components A and B scale together and die together?
├── NO ➔ Split them into different Pods (connect via a Service IP).
└── YES ➔ Valid to combine into a Multi-Container Pod.
Single-Container Pods: The Gold Standard of Modern Workloads #
Applying the one Pod, one container pattern is the safest and most efficient way to run microservices. This pattern follows the single responsibility principle at the infrastructure level.
The main advantages of a Single-Container Pod include:
- Fault Isolation: If our application container crashes, the Kubelet can easily track the failure without affecting other innocent containers.
- Precise Scalability: We can scale only the busy component (e.g. scaling the API Gateway to 10 replicas while the background worker stays at 2 replicas), significantly saving cluster RAM and CPU allocation.
- Clear Observability: CPU, RAM metrics, and log output (stdout/stderr) purely represent one application process, making debugging and monitoring alert creation easier.
Resources Shared Within One Pod #
When several containers are placed in the same Pod, Kubernetes automatically configures the Linux OS so those containers can transparently share resources through Linux Namespaces.
1. Shared Network Namespace (Localhost Communication) #
All containers in a Pod share the same IP address, the same routing table, and the same loopback network interface (lo).
- Low-Latency Communication: Container A can communicate with Container B in the same Pod directly over TCP/UDP at the
localhostor127.0.0.1IP address. - Port Conflict Consequence: Because they share the same network namespace, two containers in one Pod must not listen on the same port. If Container A uses port
8080, Container B fails to start due to an Address Already in Use conflict.
Here’s a visualization of inter-container communication within one Pod network namespace:
sequenceDiagram
participant Client as External Cluster Client
participant PodIP as Pod IP (10.244.1.42)
box rgb(240, 240, 240) Multi-Container Pod
participant Main as Main Container (Port 8080)
participant Sidecar as Sidecar Container (Port 9090)
end
Client->>PodIP: GET 10.244.1.42:8080
PodIP->>Main: Forward to Port 8080
Main-->>PodIP: HTTP 200 OK
PodIP-->>Client: HTTP 200 OK
Note over Main, Sidecar: Internal communication via Localhost
Main->>Sidecar: POST localhost:9090 (Send Log/Metrics)
Sidecar-->>Main: ACK (Very Fast & No Physical Network Latency)2. Shared Storage Volumes (File Sharing) #
Containers in a Pod can access the same storage directory by mounting the same volume in the manifest spec. This pattern is very useful for collaborative file processing.
For example, imagine a static Nginx web application (main container) that needs to serve content dynamically downloaded from a Git repository by a helper container (sidecar). We can connect them using an emptyDir volume:
apiVersion: v1
kind: Pod
metadata:
name: dynamic-web-pod
spec:
volumes:
- name: web-content
emptyDir:
medium: Memory # Uses a RAM disk (tmpfs) for very fast I/O
containers:
- name: web-server
image: nginx:1.25
volumeMounts:
- name: web-content
mountPath: /usr/share/nginx/html # Nginx reads HTML files from here
readOnly: true
- name: git-sync-sidecar
image: k8s.gcr.io/git-sync/git-sync:v3.6.0
env:
- name: GIT_SYNC_REPO
value: "https://github.com/example/static-assets.git"
- name: GIT_SYNC_DEST
value: "html"
volumeMounts:
- name: web-content
mountPath: /tmp/git # The sidecar writes cloned files here
3. Shared IPC & PID Namespace #
By default, processes in one container are isolated from processes in other containers. However, Kubernetes supports PID namespace sharing so one container can detect and manage processes in another container within the same Pod by enabling the shareProcessNamespace: true flag.
When this flag is enabled:
- Processes from all containers in the Pod are visible to each other when running
ps axor looking at the/procdirectory. - PID 1 is no longer held by our main application, but by the pause container (Kubernetes’ minimal infrastructure container).
- Helper containers can send system signals (like
SIGTERMorSIGHUP) to the main container. For example, a debugger container can runkill -HUP 12to make an Nginx process reload its configuration without killing the container.
apiVersion: v1
kind: Pod
metadata:
name: debug-target-pod
spec:
shareProcessNamespace: true # Allows inter-container process signal interaction
containers:
- name: app
image: my-app:v1
- name: debugger
image: alpine
command: ["sh", "-c", "sleep 3600"] # The debugger can detect the app PID via 'ps ax'
Three Valid Multi-Container Design Patterns #
The Kubernetes community recognizes three main design patterns where multi-container Pod usage is considered valid and highly recommended in production:
1. Sidecar Pattern #
The Sidecar pattern places an additional container (sidecar) to accompany and enhance the main container’s functionality without modifying the main container’s code.
- Real Use Cases: Logging agents, Cloud SQL Proxy (to secure GCP/AWS database connections), Prometheus Metrics Exporters, and Service Mesh proxies (Envoy).
spec:
containers:
- name: main-api
image: payment-api:v2.0
- name: log-forwarder
image: fluent-bit:3.0
# This sidecar reads log files from the shared volume
# and sends them to Elasticsearch/Loki asynchronously.
2. Ambassador Pattern #
The Ambassador pattern places an additional container acting as the network proxy representative handling all outbound connections from the main container to the outside world.
flowchart LR
subgraph Pod["Multi-Container Pod"]
MainApp["Main Container\n(Only knows localhost:5432)"] -->|"Local Connection"| Ambassador["Ambassador Container\n(pgbouncer)"]
end
subgraph DBCluster["External Infrastructure"]
Ambassador -->|"Connection Pooling\n& TLS Encryption"| DB1["PostgreSQL Primary"]
Ambassador -->|"Load Balancing"| DB2["PostgreSQL Replica"]
end- Benefits: The main container only needs to hit
localhost:5432. Connection pool ordering, database password rotation, failover algorithms, and outbound mTLS encryption are entirely handled by the Ambassador container.
3. Adapter Pattern #
The Adapter pattern is used when the main container produces output data in a non-standard format (e.g. a legacy application with raw log format), and the Adapter container normalizes that output so it can be read by the cluster’s centralized monitoring system.
For example, imagine an old application writing audit logs in raw Apache Common Log text format. We can place a Fluent Bit adapter to convert that raw text into structured JSON before sending it to the central logging system:
apiVersion: v1
kind: Pod
metadata:
name: legacy-app-pod
spec:
volumes:
- name: log-dir
emptyDir: {}
containers:
- name: legacy-app
image: legacy-app:v1.0
volumeMounts:
- name: log-dir
mountPath: /var/log/legacy
- name: log-adapter
image: fluent-bit:3.0
volumeMounts:
- name: log-dir
mountPath: /var/log/input
# The adapter reads the raw text files, does regex parsing,
# and exposes them back to the monitoring system
Initialization Order and the Native Sidecar Lifecycle #
In older Kubernetes versions (before v1.28), there was no guarantee of startup order between regular containers. This often triggered application startup errors: if the main container started faster than the network proxy sidecar (like Istio/Envoy), the main container crashed because it failed to reach the external database during initial setup.
The Modern Solution: Native Sidecar (K8s 1.29+) #
Kubernetes introduced Native Sidecar support by leveraging the initContainers block marked with the restartPolicy: Always parameter.
Here’s a comparison of the helper container lifecycle before and after Native Sidecar support:
| Comparison Dimension | Old Model (Regular Containers) | New Model (Native Sidecar K8s 1.29+) |
|---|---|---|
| Manifest Declaration | Under the spec.containers array | Under the spec.initContainers array with restartPolicy: Always |
| Startup Order | Runs in parallel with no order guarantee | Runs first; the main container waits for the sidecar to be ready |
| Health Checking | Main container can start while the sidecar is still booting | Main container is held until the sidecar’s readiness probe succeeds |
| Termination Cycle | Shut down randomly (sidecar can die first) | Main container is shut down first, sidecar last |
Native Sidecar implementation manifest:
spec:
initContainers:
- name: network-proxy
image: envoy:v1.28
restartPolicy: Always # Key marker that this init container acts as a Persistent Sidecar
readinessProbe:
httpGet:
path: /ready
port: 15021
containers:
- name: main-api
image: payment-api:v2.0
Anti-Patterns in Multi-Container Usage #
Here are two fatal container placement mistakes in production clusters:
Anti-Pattern 1: Combining Two Independent Microservices into One Pod #
Trying to merge two standalone business services for the sake of easy local communication.
ANTI-PATTERN: Putting user-service and order-service in One Pod Spec
// WHAT WE DO:
- Combine the user-service and order-service code into a single Pod manifest.
// THE CONSEQUENCES IN PRODUCTION:
- Poor Scalability: If order-service traffic spikes sharply while user-service is quiet,
we're forced to duplicate RAM and CPU for both services wastefully.
- Cascading Damage: If user-service suffers a memory leak and triggers OOMKilled,
the Kubelet restarts the entire Pod, causing unnecessary downtime for the healthy order-service.
✓ THE RIGHT SOLUTION:
- Separate each service into its own Deployment.
- Use a Kubernetes `Service` object to facilitate inter-service communication through the cluster's internal DNS:
http://user-service.default.svc.cluster.local
Anti-Pattern 2: Combining a Stateless App with a Stateful Database in One Pod #
Trying to avoid network routing overhead for database access.
ANTI-PATTERN: Putting nginx-app and postgres-database in One Pod
// WHAT WE DO:
- Write an nginx container and a postgres container in the same Pod manifest.
// THE CONSEQUENCES IN PRODUCTION:
- Data Loss: Postgres needs a persistent volume. If we scale the nginx Deployment to 3 replicas,
all three Pods try to compete for and lock the same database storage volume simultaneously, triggering data corruption.
- Slow Restart Cycles: The dynamic nginx web app is often redeployed (e.g. 5 times a day).
This forces the Postgres database to go through repeated die-and-revive cycles, damaging database caching performance.
✓ THE RIGHT SOLUTION:
- Place the web application in a `Deployment` object (stateless).
- Place the PostgreSQL database in a separate `StatefulSet` object with its own `volumeClaimTemplates`.
Summary #
- The Single Unit Principle — All containers in a Pod share the same lifecycle, node scheduling, and scaling capacity without being separable.
- Use Single Containers — The one-container-per-Pod pattern is the best standard for microservices to maintain fault isolation and precise resource allocation.
- Localhost Communication — Containers in one Pod share the same network namespace, enabling very low-latency inter-container communication at
127.0.0.1.- Port Sharing Policy — Beware of network port conflicts inside one Pod; two containers can’t use the same TCP/UDP port.
- Valid Design Patterns — Use multi-container only for three valid architectural patterns: Sidecar (helper companion), Ambassador (proxy representative), and Adapter (format adapter).
- Use Native Sidecar — Leverage
initContainerswithrestartPolicy: Always(K8s 1.29+) to guarantee sidecar startup order before the main application.- App vs Database Isolation — Never combine a stateless application and a stateful database in one Pod to avoid storage volume locking conflicts.