Kubernetes pods explained
What a pod actually is, why it can hold more than one container, the phases it moves through, and why you almost never create one by hand.
In this series
Kubernetes from scratch with k3sYou spent the last three parts standing up a cluster: the control plane that holds desired state, the nodes that run the work, and a k3s install that ties them together. Now there is finally something to run on it. The first object you meet is the pod, and almost every tutorial gets it slightly wrong by treating "pod" and "container" as synonyms. They are not. A pod is the box your containers live in, and understanding the box — what it shares, how it ages, and why it never comes back once it dies — is the thing that makes the rest of Kubernetes click.
level:Intermediate verified:Jun 2026
ℹ️ Read-along — concepts, with small examples you can try on your k3s cluster if you have one, but nothing here is required setup.
What a pod actually is
A pod is the smallest deployable unit in Kubernetes. You do not schedule a container directly; you schedule a pod, and the pod wraps one or more containers that share a context. The cluster never places half a pod on one node and half on another — a pod is atomic, scheduled as a single unit onto a single node, and all its containers live and die together on that node.
The two things the containers in a pod share are the parts worth memorising. First, they share a network namespace: the pod gets one IP address, and every container inside it sees the same network interface and can talk to its neighbours over localhost. Two containers in the same pod cannot both bind port 80 — they are on the same network stack. Second, they can share storage volumes: a volume declared on the pod is mountable by any container in it, which is how a sidecar can write files that the main container reads.
Why a pod can hold more than one container
If a pod usually has exactly one container, why does the abstraction allow more? Because some helpers only make sense glued to a specific app instance. The canonical pattern is the sidecar: a logging agent, a proxy, or a data puller that runs alongside the main container, shares its network and a volume, and scales with it one-to-one. The classic examples are data pullers, data pushers, and proxies that assist a primary application.
Under the hood, the shared context is held open by a tiny pause container that Kubernetes starts first. It does nothing but hold the network and other namespaces so they survive an app container restarting — the pod keeps its IP even when the real container crashes and comes back.
flowchart TB
subgraph POD["Pod — one IP, scheduled as a unit"]
P["pause container<br/><i>holds the namespaces</i>"]
A["app container<br/><i>binds :8080</i>"]
B["sidecar container<br/><i>log shipper</i>"]
V["shared volume<br/><i>/var/log</i>"]
end
A -->|"localhost"| B
A -->|"writes"| V
B -->|"reads"| V
P -.->|"owns net namespace"| A
P -.->|"owns net namespace"| B
Both containers share one IP and one volume because the pause container holds the namespaces open. They talk over localhost, not over a Service.
ℹ️ Note "One container per pod" is the right default. Reach for a second container only when it must share the first one's network and lifecycle — a sidecar. If two things scale independently, they belong in separate pods.
💡 Tip Because every container in a pod shares one IP, port conflicts are real. If two containers both want :8080, they cannot live in the same pod — give them their own pods and a Service in front.The pod lifecycle: phases
A pod is not just up or down. It moves through a small set of high-level phases that summarise where it is in its life. There are five of them, and the STATUS column of kubectl get pods is reporting against this model.
- Pending — the pod has been accepted by the cluster but is not yet running. This covers waiting to be scheduled onto a node and time spent pulling the container images.
- Running — the pod is bound to a node and all its containers have been created; at least one is running or starting.
- Succeeded — every container has terminated successfully and will not be restarted. This is the normal end state for a batch job.
- Failed — all containers have terminated and at least one exited with an error (and the restart policy is not bringing it back).
- Unknown — the control plane could not reach the kubelet on the node, so the pod's state cannot be determined. This usually means the node fell off the network.
stateDiagram-v2
[*] --> Pending: scheduled, image pull
Pending --> Running: containers started
Running --> Succeeded: exit 0, no restart
Running --> Failed: exit non-zero
Running --> Unknown: node unreachable
Succeeded --> [*]
Failed --> [*]
The phase is a coarse summary, not a full state machine. A long-running web server should sit in Running; a batch job marches to Succeeded; a crash you cannot recover from lands in Failed.
⚠️ Warning The phase is deliberately high-level. It is not a comprehensive state machine and does not capture per-container detail likeCrashLoopBackOff— that lives in the container statuses you see underkubectl describe.
Inspecting a pod with kubectl
You can create a bare pod straight from the command line. Since Kubernetes 1.18, kubectl run creates only a pod — the old behaviour of generating a Deployment was removed.
# Create a single bare pod running nginx
kubectl run web --image=nginx
# List pods and watch the STATUS column move through the phases
kubectl get pods
kubectl get pods -o wide # also shows node + pod IP
# Full detail: events, container statuses, restart counts
kubectl describe pod web
# Stream the logs from the pod's container
kubectl logs web
kubectl logs web -f # follow, like tail -f
kubectl get gives you the phase at a glance; kubectl describe is where you go when something is stuck in Pending (no node has room, or an image will not pull) or looping on a crash. The Events section at the bottom of describe is almost always where the answer is.
💡 Tip kubectl describe pod is the single most useful debugging command. Read the Events list from the bottom up — scheduling failures, image-pull errors, and failed liveness probes all surface there before they show up anywhere else.Why you rarely create bare pods
Here is the part that reframes everything: the pod you just created with kubectl run is a bare pod, and it has no safety net. Nothing is watching it. If the node it lives on reboots, or the container exits and the restart policy gives up, that pod is simply gone. The cluster will not recreate it, because nothing recorded that you wanted a pod to exist there — you only ever asked for one, once.
That is the difference between imperative and declarative. A bare pod is a one-shot instruction. What you actually want is a controller that holds a desired state — "keep one healthy copy of this running" — and continuously reconciles reality toward it. That controller is the Deployment, and it is the subject of the next part.
⚠️ Warning A bare pod that dies stays dead. There is no controller watching a kubectl run pod, so a node reboot or a fatal crash removes it permanently. Bare pods are fine for a five-minute debug shell; they are wrong for anything you want to keep running.A minimal pod manifest, just to show the shape — useful to read, but you would normally let a Deployment generate this for you instead:
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
A short close
A pod is the box, not the container: one IP, optional shared storage, one or more containers that live and die together, moving through Pending, Running, and then Succeeded, Failed, or Unknown. The single most important fact about a bare pod is the one that pushes you to the next part — it does not heal. To get the self-healing, replicas, and rolling updates that everyone actually wants from Kubernetes, you wrap pods in a controller. That is Deployments and self-healing, the next stop. If you need to back up to how the scheduler decides which node a pod lands on, revisit the control plane and nodes, and the series hub has the whole path.