Part I — The engine and the unit
The Pod and the Node: the Atom and the Hands
Lesson 01 gave us the container: an image (a frozen, layered filesystem plus a start command) run under namespaces for isolation and cgroups for limits — a unit that is identical everywhere and can't trample its neighbors. But it left two cracks. First, a real app is rarely one container: a web server needs a log shipper to tail its files and a proxy to terminate TLS — helpers that must share its network and disk and live and die with it, yet a container isolates each of those concerns into its own separate world. Second, a container spec is just inert text; nothing in lesson 01 actually pulls the image and runs it. This lesson derives the two primitives that close both cracks: the Pod (the atom the cluster schedules) and the Node (the machine, and the agent — the hands — that turn desired text into a running process).
New capability: the Pod — the smallest schedulable unit: a group of containers sharing one network namespace (one IP, talk over localhost), IPC, and volumes — and the Node: a machine whose kubelet runs the lesson-00 reconcile loop for its assigned Pods against a real container runtime, reporting actual state upward.
Forces next: a Pod assignment and the truth the kubelet reports must live somewhere durable and consistent that every actor agrees on — lesson 03.
1 · Why one container is the wrong atom
Take a concrete app: an HTTP server that writes structured logs to /var/log/app/. Two helpers naturally attach to it. A log shipper tails those files and forwards them to a central system. A proxy (sometimes called an ambassador) sits in front, terminating TLS on port 443 and forwarding plaintext to the server on port 8080. These three processes are not independent services — they are one logical unit. They must start together, stop together, land on the same machine, and the helpers must reach the main container's disk (the logs) and localhost port (8080).
But lesson 01's container does the opposite. Each container gets its own mount namespace (its own filesystem view) and its own network namespace (its own loopback, its own ports, its own IP). Run the shipper as a separate container and it sees an empty /var/log/app/; run the proxy separately and localhost:8080 from the proxy's view is the proxy itself, not the server. The isolation that made the container a clean unit now severs helpers that must cooperate. Scheduling them independently makes it worse: the scheduler could place the proxy on node A and the server on node B, and now "in front of" means a network hop instead of localhost.
The fix is not to weaken the container. It is to introduce a group that the cluster treats as one schedulable thing, inside which a chosen set of namespaces is shared rather than isolated. That group is the Pod — the smallest unit Kubernetes ever schedules. You never schedule a bare container; you schedule a Pod, and the Pod is placed on exactly one node, as a unit.
The rule to carry forward: a Pod ≠ a container. Most Pods hold exactly one main container, but the Pod is the boundary of co-location and the unit of an IP. The number that matters: one IP per Pod, not one IP per container. That has a concrete cost. If you ran our 3-container app as a Pod, the cluster spends one IP, not three; run 1,000 such Pods and you consume 1,000 cluster IPs total, not 3,000. It also fixes the accounting unit for resources: a Pod's CPU/memory request is the sum of its containers' requests (plus the negligible pause container), and the scheduler in lesson 10 places that one summed number, never the containers individually. One Pod, one IP, one placement decision, one fate.
2 · The pause container: how one IP is shared
Sharing a network namespace raises a lifecycle problem. If container A "owns" the namespace and the others join it, then when A crashes and restarts the namespace is destroyed and recreated — the Pod's IP would churn every time any container restarted, and the join order would matter. That is fragile. Kubernetes solves it with a tiny, near-invisible pause container (also called the sandbox or infra container).
When the kubelet creates a Pod it first starts the pause container — a few-kilobyte image whose only job is to pause() and hold the namespaces open. Every real container in the Pod then joins the pause container's network (and optionally IPC) namespace. The pause container almost never dies, so the namespace — and therefore the Pod IP — is stable across restarts of any application container. Your log shipper can crash and be recreated 50 times; the Pod keeps the same IP because the pause container, not the shipper, holds the network namespace.
pause container; it acquires the Pod's network namespace and the CNI plugin assigns one IP--net=container:pause (and shared IPC), so they all see the same loopback, ports, and IPThis is why "containers in a Pod talk over localhost" is literally true: they are in the same network namespace, so 127.0.0.1 is shared. It is also why two containers in one Pod cannot both bind port 8080 — there is one port space, like two processes on one host.
Be precise about which namespaces are shared, because it is a deliberate choice, not all-or-nothing. By default a Pod's containers share the network and IPC namespaces (via the pause container) and any declared volumes, but each keeps its own PID namespace, mount root, and cgroup — so container A cannot kill a process inside container B, and B's crash and OOMKill (lesson 01) are scoped to B's own memory limit. You can opt into a shared PID namespace (shareProcessNamespace: true) when a sidecar genuinely needs to see the main process tree, but the default keeps the failure boundaries from lesson 01 intact inside the Pod while still letting helpers cooperate over localhost and shared disk. The Pod shares exactly what cooperation needs and isolates everything else.
3 · The patterns the Pod exists to enable
The Pod is not "containers, but plural for convenience." It exists specifically to support helper containers that augment a main container while sharing its fate. These are conventional patterns, not Kubernetes API objects — the shape comes entirely from shared namespaces and shared volumes.
| Pattern | What the helper does | What it shares to do it |
|---|---|---|
| Sidecar | Runs alongside the main container for its whole life — e.g. a log shipper, a metrics exporter, a service-mesh proxy that handles all in/out traffic. | Volumes (read the main container's log files) and/or network (intercept traffic on localhost). |
| Init container | Runs to completion before the main containers start — e.g. wait for a dependency, run a schema migration, fetch config into a shared volume. | Volumes — writes setup into an emptyDir the main container then reads. Runs sequentially, must exit 0. |
| Ambassador / proxy | Sits in front of (or beside) the app and brokers external connections — TLS termination, connection pooling, sharding. | Network — the app talks to localhost and the ambassador handles the outside world. |
The payoff: the main container stays simple and single-purpose (lesson 01's clean unit), while cross-cutting concerns live in separate, independently-built images that ride along in the same Pod. They start together, stop together, and are scheduled together — exactly the "one logical unit" §1 demanded. A modern note: native sidecar containers (a special init container with restartPolicy: Always, stable since Kubernetes 1.29) fixed the old ordering bugs, ensuring sidecars start before the app and shut down after it.
4 · The Node: the machine and the hands
A Pod is still just desired-state data — JSON the cluster has decided should run. Something physical must pull the images, start the pause container, wire the CNI, run the app containers under their cgroups, and keep watching. That something is the Node: a worker machine (a VM or bare metal) running three components.
Concretely, when a Pod is assigned to a node, the kubelet drives this sequence against the CRI and CNI:
pause container; invokes the CNI plugin to assign the Pod IPrestartPolicy, and reports actual Pod status (Running/Ready/Failed) and node health back upwardThat last step is the whole point. The kubelet does not just create things and forget them — it is a control loop. If a container exits, the kubelet recreates it (this is where lesson 00's loop physically lives, per Pod, per node). And it continuously publishes actual state: this Pod's containers are Running, this node has 14 GB of allocatable memory and 6 of 32 Pod slots free. The desired state flows down to the kubelet; the actual state flows up from it.
Those reported numbers are not decoration — they are the level the rest of the cluster reads. Work an example: a node advertises 32 GB of memory, but the kubelet subtracts what the OS and its own daemons need (call it 2 GB reserved), so it reports 30 GB allocatable — the real budget Pods can claim. The node also caps Pods it will run (the default is 110 per node). The kubelet keeps publishing both the capacity and what is currently used, because the scheduler in lesson 10 places new Pods purely from these reported levels: it never inspects a node directly, it reads the node's last-reported allocatable-minus-used. If the kubelet stops reporting (the node went silent), after a grace period the cluster marks the node NotReady and treats its Pods as gone — which is exactly the lesson-00 level-triggered reaction to a missing signal, now driven by a real heartbeat. The kubelet is both the hands (it runs the containers) and the eyes (its report is the actual state everyone else trusts).
5 · The key insight: inert data vs. the hands
Hold the two halves of this lesson side by side and the architecture of all of Kubernetes appears in miniature.
DESIRED STATE (inert data) ACTUAL STATE (reported by the hands)
-------------------------- ------------------------------------
Pod spec: "run these 3 containers, kubelet: "pod web-7f is Running,
share net+vol, on some node" 3/3 containers up, node has 6 slots free"
| ^
| assigned to a node | status reported up
v |
+-------------------------------------------------- NODE (worker machine) ------------------+
| |
| kubelet --- CRI (gRPC) ---> containerd (runtime) kube-proxy CNI plugin |
| (the reconcile loop; (pulls images, asks (programs (assigns |
| runs assigned pods) kernel for ns+cgroups) Service rules) pod IP) |
| | |
| v |
| +--------------------------- POD (one IP: 10.1.4.7) -------------------------+ |
| | [pause] holds the shared network + IPC namespaces | |
| | ^ ^ ^ | |
| | | | | all join the same netns => talk over localhost | |
| | [server] [proxy sidecar] [log-shipper sidecar] | |
| | :8080 :443->:8080 tails shared volume /var/log/app | |
| | \________________ shared volume: emptyDir /var/log/app ____________/ | |
| +-------------------------------------------------------------------------+ |
+--------------------------------------------------------------------------------------------+
A Pod spec is inert desired-state data — a description, the same JSON whether or not anything runs it. The kubelet is the hands that make it real on a node, and the eyes that report what is actually true back up. This split — declared intent as data, an agent that reconciles it locally and reports up — is the lesson-00 loop made physical, and it is the template every higher-level controller in this track will copy. Three facts to lock in: a Pod is not a container (it is a group sharing one IP); there is exactly one IP per Pod (containers share it via the pause container over localhost); and a node is a worker machine whose kubelet runs assigned Pods and reports their truth.
Failure modes & checklist
Failure modes
- Treating a Pod as one container. Putting unrelated services in one Pod "to save Pods," so they can't scale or restart independently and one crash takes the others' fate. Signal: you scale the web tier and your batch job scales with it; one container's OOMKill restarts a sibling that was fine.
- Port collision inside a Pod. Two containers in one Pod both bind 8080. One fails to start because there is a single shared network namespace, not one per container. Signal: "address already in use" in the second container's logs at startup.
- Expecting a stable Pod IP across reschedule. The pause container keeps the IP stable across container restarts, but if the whole Pod is deleted and recreated (node dies, rescheduled) it gets a new IP. Hardcoding a Pod IP breaks. Signal: clients fail after a node drain or Pod eviction; the IP they cached is gone.
- Helper can't see the data. A log-shipper sidecar shows nothing because you forgot to mount the shared volume into both containers — separate mount namespaces mean separate filesystems. Signal: main container writes logs; sidecar's path is empty.
- Believing the kubelet schedules. Assuming the per-node kubelet decides placement. It only runs Pods already assigned to its node and reports status; it does not choose nodes (that is the scheduler, lesson 10). Signal: confusion about why an unschedulable Pod sits Pending with no kubelet involved.
Checklist
- One concern per Pod. Group containers only when they truly share fate, network, or disk (sidecar/init/ambassador) — otherwise give each its own Pod.
- Address siblings via localhost and give each container a distinct port; never two binds on the same port in one Pod.
- Share data via a declared volume mounted into every container that needs it; don't assume a shared filesystem just because they share a network.
- Use init containers for one-shot setup that must finish before the app, and native sidecar (
restartPolicy: Alwaysinit) containers for long-running helpers that must outlast startup. - Treat the Pod IP as ephemeral — stable across container restarts, not across reschedules. Reach Pods through the stable abstractions coming in lesson 06.
- Remember the split: the Pod spec is data; the kubelet + runtime are the hands that realize it and report actual state up.
Checkpoint exercise
Where this points next
We now have an atom (the Pod) and hands (the node's kubelet) to realize it. But a fault line is wide open. The kubelet on node A holds the desired specs for its Pods and reports actual status up — but up to where? Desired state (what an operator declared) and actual state (what every kubelet observes) both need a single, durable, consistent home. If two actors each keep their own copy, they disagree: one thinks Pod web-7f is on node A, another already rescheduled it to node B, and they fight. A node can also die and take its local view with it. Lesson 03, The Control Plane: One Source of Truth, derives the consistent store (etcd) and the single writer in front of it (the API server) that every kubelet, controller, and user agrees on — the place the desired Pod assignment lives and the actual status is recorded.
Interview prompts
- Why is the Pod, not the container, Kubernetes' smallest schedulable unit? (§1 — a real app is often cooperating containers (server + log shipper + proxy) that must share network/disk and the same fate; a bare container isolates each completely, so the Pod groups them, shares a chosen set of namespaces and volumes, and is scheduled as one onto one node.)
- How do containers in a Pod communicate, and what's the constraint? (§1–2 — they share one network namespace, so they reach each other over localhost on different ports; the constraint is one shared port space, so two containers can't both bind the same port.)
- What is the pause container and what problem does it solve? (§2 — a tiny infra container that holds the Pod's shared network/IPC namespaces; app containers join it, so the Pod's single IP stays stable even when an app container crashes and the kubelet restarts it.)
- Contrast a sidecar with an init container. (§3 — a sidecar runs for the Pod's whole life alongside the main container (log shipper, mesh proxy), sharing volumes/network; an init container runs to completion before the main containers, sequentially, for one-shot setup like migrations or fetching config into a shared volume.)
- What does the kubelet do, and what does it explicitly not do? (§4 — it runs the reconcile loop for Pods assigned to its node (start/restart containers via the CRI, run probes) and reports actual Pod/node status upward; it does not choose which node a Pod lands on — the scheduler does that.)
- What is the CRI and why does it exist? (§4 — the Container Runtime Interface, a stable gRPC contract between the kubelet and a runtime like containerd/CRI-O, so Kubernetes isn't bound to a single runtime implementation.)
- Explain "a Pod spec is inert data; the kubelet is the hands." (§5 — the spec is a declared description identical whether or not anything runs it; the kubelet on the assigned node actually realizes it against the runtime and reports the resulting actual state back up — declared intent down, observed truth up, the lesson-00 loop made physical.)