Pods
The unit of scheduling, and what a pod really is.
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.
kubectl run web --image=nginx:1.27 --port=80
pod/web created
kubectl get pod web -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATESweb 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.
apiVersion: v1kind: Podmetadata:name: sidecar-demolabels:app: webspec:containers:- name: appimage: nginx:1.27ports:- containerPort: 80resources:requests:cpu: 100mmemory: 128Milimits:cpu: 500mmemory: 256Mi- name: probeimage: curlimages/curl:8.11.1command: ["sleep", "3600"]
kubectl apply -f sidecar-demo.yaml
pod/sidecar-demo created
kubectl exec sidecar-demo -c probe -- curl -s -o /dev/null -w "%{http_code}\n" localhost:80
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.
kubectl describe pod payments-api | sed -n '/State:/,/Requests:/p'
State: RunningStarted: Thu, 16 Jul 2026 09:14:02 +0000Last State: TerminatedReason: OOMKilledExit Code: 137Finished: Thu, 16 Jul 2026 09:14:01 +0000Restart Count: 5Limits:memory: 256MiRequests:memory: 256Mi
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.
$ kubectl run web --image=nginx:1.27 --port=80$ kubectl get pod web -o wideapiVersion: v1kind: Pod$ metadata:name: sidecar-demolabels:app: web$ spec:containers:- name: appimage: nginx:1.27ports:- containerPort: 80resources:requests:cpu: 100mmemory: 128Milimits:cpu: 500mmemory: 256Mi- name: probeimage: curlimages/curl:8.11.1command: ["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.