Pods

The unit of scheduling, and what a pod really is.

Beginner12 min · lesson 7 of 65
In plain terms
A pod is a lunchbox: usually one main container, sometimes a small helper beside it, sharing the same box (network and files). Kubernetes hands out and throws away whole lunchboxes — never individual sandwiches.

Kubernetes will not run a container for you. The smallest thing it puts on a machine is a Pod, and every container you run lives inside one. There is no such thing as a loose container floating around a cluster on its own. Get that straight now, and half the confusion that shows up later, about IP addresses and restarts and why a Pod seems to have moved, just quietly goes away.

A Pod is a bit like an apartment shared by two roommates. One street address, one phone line, one kitchen they both cook in. But each roommate is still a separate person having their own day. Containers in a Pod work the same way. They share one IP address (Internet Protocol address, the number that identifies the Pod on the network), they talk to each other over localhost (the loopback address a machine uses to reach itself), they can mount the same storage volumes, and they always land on the same node, together, or not at all. Each container is still its own process running its own image.

What a pod is under the hood

Start with one piece of Linux. A namespace is a private view of a single system resource, like handing a program its own room and telling it that room is the whole house. A network namespace is a private network stack: its own interfaces, its own IP, its own loopback. Here is the part you never see in kubectl. When the kubelet (the agent Kubernetes runs on every node) starts your Pod, it launches a tiny throwaway container first. It's called the pause container, and its only job is to hold that network namespace open. Every real container in the Pod then joins the pause container's namespace instead of making its own. That's the whole trick behind shared networking. One namespace, one IP, everybody on localhost.

The pause container also explains restarts. If your app container crashes, the kubelet starts it again right where it was, and because the pause container never died, the network namespace and the Pod's IP live straight through that restart. The IP only goes away when the whole Pod is deleted. And a Pod is never picked up and moved to another node. It gets deleted in one place, and a brand new Pod, with a new identity and usually a new IP, is created somewhere else. Pods are cheap, and they're built to be thrown away.

terminal
kubectl run web --image=nginx:1.27 --port=80
output
pod/web created
terminal
kubectl get pod web -o wide
output
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web 1/1 Running 0 18s 10.244.1.37 node-1 <none> <none>

That STATUS column walks through a short set of phases. Pending means the API server (the cluster's front door for every request) has accepted the Pod, but nothing is actually running yet, usually because the scheduler hasn't placed it on a node or the image is still downloading. Running means it's bound to a node with at least one live container. Succeeded and Failed are the end of the line, and you mostly meet them with Jobs, the one-shot workloads that finish and stop. A Pod stuck in Pending is the classic why-won't-this-start puzzle, and nine times out of ten the answer is sitting in the events at the bottom of kubectl describe pod.

One pod, more than one container

Most Pods hold exactly one container. You add a second only when a helper genuinely shares the main app's life and network. A sidecar that ships logs off to somewhere central. A little proxy that sits in front and handles the encrypted traffic. The test is simple. If two processes have to live and die together and want to talk over localhost, put them in one Pod. If either one could scale on its own, keep them in separate Pods. The requests block in the YAML below is what the scheduler reads to pick a node, a reservation it makes on your behalf. The limits are the hard ceiling the kernel enforces once the container is actually running.

sidecar-demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: sidecar-demo
labels:
app: web
spec:
containers:
- name: app
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
- name: probe
image: curlimages/curl:8.11.1
command: ["sleep", "3600"]
terminal
kubectl apply -f sidecar-demo.yaml
output
pod/sidecar-demo created
terminal
kubectl exec sidecar-demo -c probe -- curl -s -o /dev/null -w "%{http_code}\n" localhost:80
output
200

The curl container just reached the nginx container on localhost, even though the two images were built by completely different people. Same network namespace, same loopback. From outside the Pod you'd hit the IP that get -o wide printed. From inside, it's only ever localhost. And notice that READY would show 2/2 here, because every container in the Pod has to be up before the Pod counts as ready.

Pods are managed, not hand-run

Everything up to now used bare Pods because they're easy to watch. In a real cluster you almost never make one by hand. A bare Pod has nobody looking after it. Delete its node, or delete the Pod, and nothing brings it back. This is where the idea that runs through all of Kubernetes shows up. Think of a thermostat: you set it to 20 degrees, it reads the room, and it keeps nudging the heat until reality matches the number you asked for. Kubernetes works the same way. You declare the state you want, and a controller keeps checking reality against it and correcting the gap. For Pods, that controller is usually a Deployment, a StatefulSet, a DaemonSet, or a Job. You tell a Deployment you want three Pods shaped a certain way, and it creates a ReplicaSet that holds exactly three, replacing any that die and rolling out new versions without dropping the service.

When you do reach for a Pod directly, it's nearly always to look, not to run. kubectl logs to read what it printed. kubectl describe pod for its events and the reason it refuses to start. kubectl exec to open a shell inside and poke around. The Pod is where your code actually runs, so troubleshooting tends to end here, even when something further up is what created it.

A container over its memory limit is killed, not slowed down
This one catches people who set memory limits by guesswork. CPU and memory act nothing alike when you hit the limit. Go past your CPU limit and the kernel just throttles you: the app runs slower but stays alive. Go past your memory limit and the kernel's OOM killer (OOM is short for Out Of Memory) kills the container on the spot. The kubelet restarts it, the Pod stays put on its node, and the RESTARTS number ticks up while STATUS still calmly reads Running. The evidence is in describe: Last State Terminated, Reason OOMKilled, Exit Code 137 (that's 128 plus signal 9, the kernel's un-ignorable kill). If a Pod keeps looping on restarts for no obvious reason, check this before anything else, then either raise the memory limit or go find the leak. Setting the limit right up against a tight request, the way the output below does, is the usual way people trip it.
terminal
kubectl describe pod payments-api | sed -n '/State:/,/Requests:/p'
output
State: Running
Started: Thu, 16 Jul 2026 09:14:02 +0000
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Finished: Thu, 16 Jul 2026 09:14:01 +0000
Restart Count: 5
Limits:
memory: 256Mi
Requests:
memory: 256Mi
How a pod goes from YAML to Running
1kubectl applyYou POST the pod spec to the…2API server + etcdThe spec is validated and…3SchedulerWatches for pods with no node,…4kubeletThe agent on that node sees…5pause containerCreated first. Holds the…6app containersImages pulled, each container…7Runningkubelet reports live status…

Pending means scheduling failed or is waiting. CrashLoopBackOff means the runtime started and the process exited. Those are different tickets.

Never treat a bare pod in production as durable. Controllers recreate; bare pods do not.

Readiness is not liveness. A pod can be alive and still withheld from Service endpoints until ready. Those are different tickets.

Try this

Run a one-off pod, describe it, and note node name, IP, and container statuses. A pod is the unit of scheduling — shared network namespace, shared volumes, one fate.

terminal
$ kubectl run web --image=nginx:1.27 --port=80
$ kubectl get pod web -o wide
apiVersion: v1
kind: Pod
$ metadata:
name: sidecar-demo
labels:
app: web
$ spec:
containers:
- name: app
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
- name: probe
image: curlimages/curl:8.11.1
command: ["sleep", "3600"]
$ kubectl apply -f sidecar-demo.yaml
$ kubectl exec sidecar-demo -c probe -- curl -s -o /dev/null -w "%{http_code}\n" localhost:80
$ kubectl describe pod payments-api | sed -n '/State:/,/Requests:/p'

Takeaway

Schedule pods, not containers. Sidecars share the pod IP and die with the pod. If you need independent lifecycles, you need separate pods.

Quick check
01A pod shows STATUS Running but its RESTARTS count keeps climbing, and kubectl describe reports Last State: Terminated, Reason: OOMKilled, Exit Code: 137. What is actually happening?
Incorrect — Eviction is a different mechanism. It deletes the pod and you would see status Evicted, not a Running pod whose container is being restarted in place with a rising count.
Correct — Exit 137 is SIGKILL from the kernel's OOM killer when the container passes its memory limit. The pod stays on its node while kubelet restarts the container. Raise the memory limit or fix the leak.
Incorrect — Exceeding a CPU limit only throttles the container. It slows down but is never killed. Only memory limits produce OOMKilled.
Incorrect — A failing readiness probe drops the pod out of Service endpoints and shows 0/1 READY, but it does not terminate the container or raise the restart count.
02When the kubelet starts a pod, it launches a tiny 'pause' container before any of your application containers. What is that pause container actually for?
Incorrect — Wrong job: restarting a crashed container is the kubelet's doing, and the pause container never probes anything.
Incorrect — Image pulls are handled by the kubelet and the container runtime, not by the pause container.
Correct — the pause container owns the network namespace that the app containers join, which is why they share one IP, reach each other on localhost, and keep that IP through an app-container restart.
Incorrect — Resource limits are enforced by the Linux kernel through cgroups, not by any container in the pod.
03A pod was Running on node-1 with IP 10.244.1.37. node-1 fails, and moments later the same managed workload is Running on node-2 with IP 10.244.2.9. What actually happened?
Incorrect — Kubernetes never live-migrates a pod: a pod is not picked up and moved between nodes.
Incorrect — A restart happens in place on the same node; it cannot relocate a pod to a different node.
Incorrect — Pod IPs are not reassigned across nodes; the old IP died with the old pod.
Correct — pods are disposable, so a controller replaced the lost pod rather than moving it, which is exactly why the identity and IP changed.

Related