DEVOPS-02-003-P · Primer — Pods
The idea in one sentence
A Pod is Kubernetes' smallest deployable unit: one or more containers that are always scheduled together on the same node, sharing one IP address (and hence localhost) and any declared volumes.
Why it exists
Before pods, container schedulers treated every container as an independent unit, so a web server and its logging or mTLS-proxy sidecar could land on different nodes, forcing them to talk over the network instead of localhost and breaking the moment one was rescheduled without the other. Kubernetes needed a way to say "these containers are one thing" — an nginx container and its envoy sidecar must always live together, restart together, and reach each other at 127.0.0.1 with no service discovery in between. The Pod is that guarantee: it's the atomic unit the scheduler places, so a pod's containers can never be split across nodes.
How it works
Under the hood, the kubelet doesn't start your containers first — it starts a hidden "pause" (infrastructure) container that does nothing but hold the pod's Linux network and IPC namespaces open. Every other container in the pod is started joining that same namespace instead of getting one of its own. That's the entire mechanism behind "one pod, one IP": the pause container owns the namespace, and app containers are just processes that happen to share it, the same way two processes on your laptop share \(127.0.0.1\). Volumes work the same way at the filesystem layer — a volume declared on the Pod spec is bind-mounted into every container that lists it, at whatever path each container asks for.
Take Pod web-7f8a, deployed with two containers: nginx listening on port 8080, and an envoy sidecar listening on port 15001 that terminates mTLS for a service mesh. Because both containers share the pod's single IP, say \(10.1.2.7\), envoy can forward decrypted traffic to localhost:8080 — no DNS lookup, no extra network hop, and no way for that hop to be intercepted off-node. kubectl get pod web-7f8a -o wide shows exactly one IP for both containers; kubectl exec -it web-7f8a -c envoy -- curl localhost:8080 proves it by reaching nginx from inside the sidecar.
This "one pod, one IP" rule is also what capacity planning is really counting. Most CNI setups hand each node a /24 slice of the pod CIDR, so \[ \text{usable IPs per node} = 2^{32-24} - 2 = 254 \] and that number caps how many pods — not containers — a node can host: a node can run 254 single-container pods or 254 five-container pods, it's still 254 IPs either way. It also explains Deployment scaling math: if a Deployment sets \(replicas = 10\) and each pod carries 3 containers (app plus 2 sidecars), you get \(10 \times 3 = 30\) running containers but only \(10\) pod IPs — the HPA and the scheduler both reason in pods, never in containers.
See it — One pod, one IP: the pause container holds the network namespace; app and sidecar share localhost and a volume.
flowchart TD N["Node<br/>(kubelet)"] --> P["Pod<br/>(IP: 10.1.2.7)"] P --> C1["pause container<br/>(holds netns)"] P --> C2["app: nginx<br/>(port 8080)"] P --> C3["sidecar: envoy<br/>(port 15001)"] C2 -->|"localhost:8080"| C3 P --> V["shared volume<br/>(emptyDir)"] C2 -->|"writes logs"| V C3 -->|"reads logs"| V
The one gotcha
Adding a sidecar to every pod's spec never changes the IP math or the HPA target — both count pods, not containers — and a rescheduled or restarted pod gets a brand-new IP every time, so nothing should ever hard-code a pod's address; use a Service instead.
The one-line takeaway
One pod, one IP — containers inside share network fate.
Where you'll meet it
kubectl get pods -o wide (one IP column per pod); kubectl exec -c <container> to jump between containers in the same pod; kubelet plus the CNI plugin (EKS/GKE/AKS) that assigns the pod IP from the node's CIDR; kube-scheduler, which places the whole pod atomically; Helm chart pod templates; k9s to browse live pods and their containers.
Named concepts: pod
