Pod vs Container in Kubernetes: What Is Shared, What Restarts
7 min readBytePatterns
Pod vs container in Kubernetes: the Pod is what gets scheduled and shares one IP, ports and volumes; containers are what run, crash and restart one by one.
Pod vs container in Kubernetes in one line: a container is one process tree from one image, isolated and resource-limited by the kernel; a Pod is the wrapper Kubernetes schedules, a group of one or more containers that land on the same node and share one network namespace (one IP, one port space, localhost), IPC and volumes. Kubernetes never schedules a bare container, and it never moves a Pod. What crashes and restarts is a container inside the Pod, and the Pod keeps its name, UID and IP while that happens. Facts come from the Kubernetes documentation listed at the end, as of October 2026.
| Question | Container | Pod |
|---|---|---|
| Unit of | running: one image, one process tree | scheduling: placed on one node as a whole |
| Has its own | root filesystem, requests and limits, probes, restartCount | IP address, hostname, volumes, restartPolicy |
| When it fails | restarted in place by the kubelet | never rescheduled; a controller creates a new one |
The problem it solves
Some processes only make sense together. An app and the proxy that terminates its mTLS. An app and the agent that ships its log files. A web server and the helper that refreshes its certificates. They must run on the same machine, reach each other cheaply and see the same files, yet you want to build, version and limit them separately. Putting both in one container image couples their releases; putting them in separate Pods puts a network hop and a scheduler decision between them. The Pod is the middle ground: separate containers, one shared environment.
The intuition
A container is about isolation. On Linux it is a process with its own namespaces and a cgroup, as the sibling article on containers vs virtual machines shows. Each container in a Pod still has its own image and root filesystem, its own CPU and memory requests and limits, its own probes and its own restart count.
A Pod is about sharing. Kubernetes creates the Pod's environment first and then starts each container inside it:
- Network: every container shares the Pod's network namespace, so they have one IP and one set of ports. Two containers cannot both listen on 8000, and the proxy reaches the app at
localhost:8000. Other Pods reach it at the Pod IP. - IPC: containers in a Pod can use SystemV semaphores or POSIX shared memory with each other; containers in different Pods cannot without special setup.
- Storage: a volume declared on the Pod, such as an
emptyDir, can be mounted by every container, and survives a container restart. - Not shared by default: the process list. Each container sees its own PIDs unless the Pod sets
shareProcessNamespace: true.
Restarts happen to containers, not Pods. The Pod's restartPolicy (Always by default, OnFailure, Never) tells the kubelet what to do when a container exits, and it applies to app containers and regular init containers. The kubelet restarts the failed container in place with an exponential back-off, 10 s, 20 s, 40 s and so on, capped at five minutes and reset after ten minutes of clean running: that waiting state is CrashLoopBackOff. The count lives on the container, in status.containerStatuses[].restartCount. A Pod whose node dies is not restarted at all: it is deleted, and its ReplicaSet makes a new Pod with a new UID and usually a new IP. Since Kubernetes 1.35, a container can also carry its own restartPolicy and exit-code rules (beta, on by default) to override the Pod's.
Three kinds of container. Init containers run one at a time, each to completion, before the app containers start, for work such as migrations or waiting on a dependency. Sidecar containers are init containers with restartPolicy: Always: they start in init order, then keep running beside the app for the Pod's whole life, support probes, and are stopped after the app containers, in reverse order. This native sidecar support has been stable since Kubernetes 1.33, and it also means a sidecar no longer stops a Job from completing. App containers are the rest.
When to use which
- One container per Pod is the default. Most Pods hold a single app container.
- Add a container to the Pod only when it must share the app's network or files and live and die with it: a service-mesh proxy, a log shipper reading a shared volume, a config reloader. Declare it as a sidecar so it starts first and stops last.
- Use an init container for one-shot work that must finish before the app starts.
- Use a separate Pod when the two scale differently or could run on different nodes, such as an app and its database.
Watch it run
The lesson's animation draws the ownership chain above the Pod. A Deployment wants three replicas; its controller creates a ReplicaSet, the ReplicaSet creates three Pods, and the scheduler places each on a node. One frame stops on a single Pod and defines it as one or more containers sharing a network namespace (localhost) and storage. Then a node fails, its Pod is simply gone, and the ReplicaSet creates a replacement with a new name and UID, which is the line between a Pod that is replaced and a container that is restarted. The last frames change the image and show a second ReplicaSet taking over.
Pods, ReplicaSets & Deployments
Step 1 of 10
You write desired state: a Deployment that wants three replicas of api.
The same interactive animation as the lesson — step through it with the controls.
The code
A Pod with all three kinds of container. migrate runs once; log-shipper and proxy are sidecars that start before app and outlive it; everyone shares the logs volume and the Pod's IP:
# illustrative: one Pod, four containers, one network namespace
apiVersion: v1
kind: Pod
metadata: { name: api, labels: { app: api } }
spec:
restartPolicy: Always # for app containers and regular init containers
volumes: [{ name: logs, emptyDir: {} }]
initContainers:
- { name: migrate, image: registry.example.com/api:1.5.0, command: ["./migrate"] }
- name: log-shipper
image: registry.example.com/shipper:2.1
restartPolicy: Always # this makes it a sidecar
volumeMounts: [{ name: logs, mountPath: /var/log/app }]
- { name: proxy, image: registry.example.com/proxy:1.9, restartPolicy: Always }
containers:
- name: app
image: registry.example.com/api:1.5.0
ports: [{ containerPort: 8000 }] # the proxy reaches it at localhost:8000
volumeMounts: [{ name: logs, mountPath: /var/log/app }]
A toy model of the shared network namespace, not a container runtime. Two containers in one Pod collide on the same port, reach each other on localhost, and share a volume; a container in another Pod cannot use that localhost and must use the Pod IP:
class NetNS:
"""Toy network namespace: one IP, one port table, one loopback."""
def __init__(self, ip):
self.ip, self.ports = ip, {}
def bind(self, container, port):
if port in self.ports:
return f"{container}: bind :{port} failed, address in use ({self.ports[port]} has it)"
self.ports[port] = container
return f"{container}: listening on {self.ip}:{port}"
def connect(self, host, port):
if host in ("localhost", self.ip) and port in self.ports:
return f"connected to {self.ports[port]}"
return "connection refused"
class Pod:
def __init__(self, name, ip, containers):
self.name, self.net = name, NetNS(ip) # ONE namespace, shared by every container
self.containers = containers
self.volumes = {"scratch": {}} # an emptyDir all containers may mount
api = Pod("api-7c9f", "10.1.0.7", ["app", "proxy"])
print(api.net.bind("app", 8000)) # app: listening on 10.1.0.7:8000
print(api.net.bind("proxy", 8000)) # proxy: bind :8000 failed, address in use (app has it)
print(api.net.bind("proxy", 15001)) # proxy: listening on 10.1.0.7:15001
print("proxy -> localhost:8000:", api.net.connect("localhost", 8000))
# proxy -> localhost:8000: connected to app
web = Pod("web-1", "10.1.3.2", ["web"])
print("web-1 -> localhost:8000:", web.net.connect("localhost", 8000))
# web-1 -> localhost:8000: connection refused
print("web-1 -> 10.1.0.7:8000:", api.net.connect("10.1.0.7", 8000))
# web-1 -> 10.1.0.7:8000: connected to app
api.volumes["scratch"]["token"] = "t-91" # app writes a file ...
print("proxy reads token:", api.volumes["scratch"]["token"]) # proxy reads token: t-91
And the lifecycle: start and stop order with sidecars, the restart back-off counted per container, and a node failure that produces a new Pod rather than moving the old one:
import itertools
uids = itertools.count(1)
def backoff(n): # delay before the n-th restart: 10s, 20s, 40s ... capped at 300s
return min(10 * 2 ** (n - 1), 300)
class Pod2:
def __init__(self, ip, init, sidecars, apps):
self.uid, self.ip = f"uid-{next(uids)}", ip
self.init, self.sidecars, self.apps = init, sidecars, apps
self.restarts = {c: 0 for c in sidecars + apps} # counted per container
def start_order(self): # init runs to completion, sidecars start and stay, then apps
return self.init + self.sidecars + self.apps
def stop_order(self): # apps first, then sidecars in reverse order
return self.apps + self.sidecars[::-1]
def crash(self, c): # restartPolicy Always: the kubelet restarts this container
self.restarts[c] += 1
return backoff(self.restarts[c])
pod = Pod2("10.1.0.7", ["migrate"], ["log-shipper", "proxy"], ["app"])
print("start:", pod.start_order()) # start: ['migrate', 'log-shipper', 'proxy', 'app']
print("app restart delays:", [pod.crash("app") for _ in range(7)])
# app restart delays: [10, 20, 40, 80, 160, 300, 300]
print(pod.uid, pod.ip, pod.restarts)
# uid-1 10.1.0.7 {'log-shipper': 0, 'proxy': 0, 'app': 7}
print("stop:", pod.stop_order()) # stop: ['app', 'proxy', 'log-shipper']
# the node fails: the Pod is not moved; its ReplicaSet creates a new one
new = Pod2("10.1.4.9", ["migrate"], ["log-shipper", "proxy"], ["app"])
print(new.uid, new.ip, new.restarts)
# uid-2 10.1.4.9 {'log-shipper': 0, 'proxy': 0, 'app': 0}
Seven crashes left the Pod's UID and IP untouched and the sidecars at zero restarts; only the replacement Pod starts the counters again.
The complexity
- Scheduling: the scheduler places the whole Pod, using the requests of all its containers together, so a Pod with a heavy sidecar needs a node with room for both; every container in a Pod always shares one node.
- Restarts: a container restart is cheap: same Pod, same IP, volumes intact, no scheduler involved. A new Pod costs scheduling, image pulls if the node lacks the image, a new IP and fresh endpoints.
- Readiness: the Pod is Ready only when its containers are ready, sidecars with readiness probes included, so one slow container keeps the whole Pod out of a Service.
Where it goes wrong
- Port clashes. Two containers in one Pod listening on the same port: the second fails to bind and crash-loops. They share one port space.
- Calling a neighbour Pod on
localhost.localhostonly reaches containers in the same Pod; another Pod is reached through its IP or, better, a Service. - Sidecars as plain containers. A log shipper declared under
containersmay start after the app and be killed alongside it, losing the last lines, and in a Job it keeps the Pod running after the work is done. Declare it as an init container withrestartPolicy: Always. - Reading
RESTARTSas Pod restarts. The number counts container restarts; the Pod was never restarted. Check which container withkubectl get pod -o jsonpathoncontainerStatuses, thenkubectl logs -c <name> --previous. - Too much in one Pod. Containers that scale independently forced into one Pod scale together, and one crash-looping helper keeps the whole Pod unready.
When it shows up in interviews
As an opener: "what is a Pod, and why not just containers?" The expected answer is the shared network namespace and volumes, the sidecar pattern, and the fact that containers restart in place while Pods are replaced. The objects above the Pod are in Pod vs ReplicaSet vs Deployment, and the controller loop that replaces a lost Pod is in the Pods, ReplicaSets and Deployments lesson.
How to say it in an interview
"A container is one image running as an isolated process. A Pod is what Kubernetes schedules: one or more containers on the same node that share one network namespace, so one IP and localhost, plus IPC and volumes. When a container exits, the kubelet restarts that container in place with back-off, and the restart count is per container. When the node dies, the Pod is not moved; its controller creates a new Pod with a new UID and IP. I put a second container in a Pod only when it must share the network or files, like a proxy or log shipper, and I declare it as a native sidecar: an init container with restartPolicy: Always."
Frequently asked questions
Can a Kubernetes Pod have multiple containers?
Yes. A Pod can hold several app containers plus init containers and sidecars. They share the Pod's IP, ports, IPC and volumes, so they talk over localhost and through shared files. Most Pods still run one app container; add another only when it must live beside the app, such as a proxy or log shipper.
Does a Pod restart when a container crashes?
No. The kubelet restarts the crashed container inside the same Pod, following the Pod's restartPolicy, with a back-off of 10 s doubling to a five-minute cap. The Pod keeps its UID and IP, and the container's restartCount goes up. A Pod is only replaced, by its controller, when it is deleted or its node fails.
What is the difference between an init container and a sidecar container?
An init container runs to completion before the app starts, one at a time. A sidecar is an init container with restartPolicy: Always: it starts before the app, keeps running beside it, supports probes, and stops after the app. Native sidecars have been stable since Kubernetes 1.33.
Sources
- Pods — Kubernetes Documentation
- Pod Lifecycle — Kubernetes Documentation
- Sidecar Containers — Kubernetes Documentation
- Init Containers — Kubernetes Documentation
- Share Process Namespace between Containers in a Pod — Kubernetes Documentation