The Pod is the smallest thing Kubernetes schedules, and almost every beginner question about Kubernetes comes back to two facts about it. First, a Pod is a group of containers that share some things and not others. Second, a Pod is disposable: it is never moved or repaired, only replaced. Once both facts are clear, Services, Deployments and StatefulSets stop looking like arbitrary extra objects and start looking like the obvious consequences.
Everything here applies to Kubernetes 1.30 and newer. The manifests parse as valid YAML, but the kubectl listings are representative rather than copied from a live cluster.
What the containers in a Pod share
A Pod with two containers runs both on the same node, and they share:
- One network namespace. One IP address, one set of ports, one
localhost. Container A can reach container B at127.0.0.1:9090, and the two can’t both listen on port 8080. - Volumes declared on the Pod. Each container chooses where (and whether) to mount them.
- A lifecycle. They’re scheduled together, and the Pod is deleted as a unit.
They don’t share a filesystem by default (each has its own image root), and they don’t share a process namespace unless you set shareProcessNamespace: true. Each container can have its own resource requests and limits.
Here’s a Pod with an app and a log-shipping helper reading the same emptyDir:
apiVersion: v1
kind: Pod
metadata:
name: web
labels:
app: web
spec:
initContainers:
- name: log-shipper
image: fluent/fluent-bit:3.1
restartPolicy: Always
volumeMounts:
- name: logs
mountPath: /var/log/app
containers:
- name: app
image: nginx:1.27
ports:
- containerPort: 80
volumeMounts:
- name: logs
mountPath: /var/log/nginx
volumes:
- name: logs
emptyDir: {}The helper is declared as an init container with restartPolicy: Always. That’s the native sidecar syntax (beta and on by default since 1.29, stable in 1.33): it starts before app, keeps running alongside it, and is stopped after app during shutdown. Before native sidecars, helpers went in containers and could exit before the app finished flushing logs, or keep a Job’s Pod running forever after the main work completed.
Why group containers at all, rather than run two Pods? Because some helpers need to be on the same machine and share a network or disk with the app: a proxy listening on localhost, a log shipper reading files, a process that refreshes a certificate in a shared volume. If two processes could be scaled separately and talk over the network, they belong in separate Pods. An interviewer asking “would you put the database in the same Pod as the app?” wants to hear no, because you’d be unable to scale the app without creating more databases.
Phase, container state and conditions
kubectl get pods prints a STATUS column, and it’s easy to treat it as the whole story. It isn’t. There are three layers.
Pod phase is a five-value summary in status.phase:
Pending: accepted by the API server, but at least one container isn’t running yet. Covers waiting for scheduling, image pulls and init containers.Running: bound to a node, all containers created, at least one running or restarting.Succeeded: all containers exited 0 and won’t restart.Failed: all containers have stopped and at least one exited non-zero (or was killed by the system).Unknown: the control plane can’t get status from the node, usually because the node is unreachable.
Container state is per container: Waiting (with a reason like ContainerCreating, ImagePullBackOff or CrashLoopBackOff), Running, or Terminated (with an exit code and reason such as Completed, Error or OOMKilled).
Conditions are booleans: PodScheduled, Initialized, ContainersReady, Ready, plus PodReadyToStartContainers (on by default since 1.29).
The STATUS column picks the most interesting of these. That’s why it can show CrashLoopBackOff, which isn’t a phase at all; the phase of a crashlooping Pod is usually still Running. You can see the layers separately:
$ kubectl get pod web -o jsonpath='{.status.phase}{"\n"}{range .status.containerStatuses[*]}{.name}{" "}{.state}{"\n"}{end}'
Running
app {"running":{"startedAt":"2026-10-07T09:12:44Z"}}A Pod can be Running and not Ready. That’s normal while a readiness probe hasn’t passed yet, and it’s exactly the state in which a Service stops sending it traffic.
Restart in place versus replace
When a container in a Pod exits, the kubelet on that node restarts it according to the Pod’s restartPolicy: Always (the default, and the only option for Deployment Pods), OnFailure or Never (both used for Jobs). Restarts happen on the same node, in the same Pod, keeping the same IP and the same emptyDir contents. The container’s own writable layer is reset. Repeated failures make the kubelet back off exponentially, 10s, 20s, 40s and so on up to five minutes, which is the CrashLoopBackOff you see in the status. The CrashLoopBackOff guide covers diagnosing that loop.
What the kubelet never does is move a Pod. If the node dies, is drained, or the Pod is evicted for memory pressure, that Pod is finished. It stays in the API as Failed or is deleted. If something needs to keep running, a controller (ReplicaSet, StatefulSet, DaemonSet, Job) notices that it has fewer Pods than it wants and creates a new one. The new Pod has:
- a new name (for Deployments, a new random suffix),
- a new UID,
- usually a new IP,
- possibly a different node,
- a fresh
emptyDirand a fresh container filesystem.
You can watch that happen:
$ kubectl get pods -l app=checkout -o wide
NAME READY STATUS IP NODE
checkout-6d8f7c9b54-k2x7p 1/1 Running 10.244.1.17 node-a
$ kubectl delete pod checkout-6d8f7c9b54-k2x7p
pod "checkout-6d8f7c9b54-k2x7p" deleted
$ kubectl get pods -l app=checkout -o wide
NAME READY STATUS IP NODE
checkout-6d8f7c9b54-9vq4m 1/1 Running 10.244.3.52 node-cA Pod you create directly with kubectl run or a bare Pod manifest has no controller, so if its node dies, nothing brings it back. That’s the one-sentence answer to “why not just create Pods?”
Designing for disposability
Treating Pods as replaceable has consequences for the application.
Don’t address Pods by IP. The IP in the example changed from 10.244.1.17 to 10.244.3.52. Clients should use a Service, which keeps a stable virtual IP and DNS name and tracks the current Pod IPs through EndpointSlices.
Don’t keep important state on the Pod’s disk. The container layer and emptyDir vanish with the Pod. Data that matters goes to a PersistentVolume, a database or object storage.
Don’t rely on in-memory sessions. If a user’s session lives in one Pod’s memory, they’re logged out when that Pod is replaced. Use a shared store like Redis, or signed tokens.
Handle SIGTERM. Since Pods are deleted routinely (every rollout, every node drain), the shutdown path runs all the time, not just in emergencies.
When a workload genuinely needs a stable name and its own disk across replacements, as databases do, a StatefulSet provides that. Even then, the Pod is still deleted and recreated; it just comes back with the same name and the same claim.
Interview practice
“A developer stores the IP of a cache Pod in a config file so their service can connect to it directly, skipping the Service ‘for performance’. It works for a week, then breaks overnight. Explain what probably happened and what you’d change.”
Answer
The cache Pod was replaced. That could be a node upgrade or autoscaler scale-down draining the node, an eviction under memory pressure, a rollout of a new cache image, or the node failing. In each case the controller created a new Pod with a new IP, and the config file still pointed to the old one, so connections time out or, worse, reach an unrelated Pod that later received that IP.
The fix is to connect through a Service name such as cache.shop.svc.cluster.local. The performance argument is weak: for a normal ClusterIP Service, kube-proxy programs iptables or IPVS rules on each node, so traffic goes straight from client Pod to backend Pod with no extra network hop. If the client needs to address individual cache Pods (for client-side sharding, say), a headless Service with a StatefulSet gives each Pod a stable DNS name like cache-0.cache.shop.svc.cluster.local, which survives replacement even though the IP doesn’t. I’d also check the cache’s data expectations: a replaced cache Pod starts empty unless it’s backed by a volume.