Visual walkthrough — Containers — namespaces, cgroups, difference from VMs
We build in this order:
- one process on the shared kernel,
- give it a private view (namespaces),
- give it a private root filesystem,
- cap what it can use (cgroups) with the CPU-fraction formula,
- cover the degenerate cases (no namespace, root-inside),
- contrast the whole thing with a VM,
- compress it into one picture.
Step 1 — Start from ONE bare process on ONE kernel

WHAT. At the bottom of every Linux machine sits the kernel: the one piece of code that owns the real hardware (CPU, RAM, disk, network card). Everything else is a process — a running program — and every process asks the kernel for things through syscalls (see Linux Kernel — syscalls).
WHY start here. A container is not a new kind of object. It is going to be this exact process with extra settings. So before we isolate anything, look at what a plain process can see: it shares global tables with every other process — one list of process IDs, one hostname, one network stack, one filesystem mount table.
PICTURE. In the figure, the blue box is the kernel; the yellow circle is our process. The dashed lines show it reaching into the kernel's global tables (drawn on the right). Right now nothing is private — our process sees the whole building. Our whole job is to cut those dashed lines and hand it private copies instead.
Step 2 — Cut ONE dashed line: the first namespace

WHAT. We ask the kernel: "Give my new child its own private copy of the process-ID table." The tool is the clone syscall with a flag bit named CLONE_NEWPID.
WHY this tool and not another. We could try to hide other processes with permissions, but they would still exist in the same numbering — our process would see PID 1, PID 2, … of the host. The only clean way is a fresh table: a namespace. A namespace is literally "a second, separate copy of one global table." Cutting that one line and handing over a blank table is exactly what CLONE_NEWPID does.
PICTURE. The figure shows the same process, but now the PID dashed line is cut (red X) and rerouted to a new, empty PID table. In that fresh table our process is renumbered — it becomes PID 1, because a brand-new list starts counting at 1.
clone— the syscall that creates a new process.CLONE_NEWPID— one flag bit; setting it says "the child gets its own PID namespace."- the
|(OR) — how you combine flags; each bit privatises one table, so OR-ing more bits privatises more tables.
Step 3 — Cut ALL the lines: the full set of namespaces

WHAT. Repeat Step 2 for every global table the kernel exposes. Each has its own flag.
WHY. From the rule above, isolation is a conjunction: you're only "alone" when every shared thing is private. Miss one and there's a hole — e.g. private PIDs but a shared network stack means you still see the host's IP addresses.
PICTURE. Seven dashed lines, seven red cuts, seven private tables:
| Flag | Private copy of | So the process now has its own… |
|---|---|---|
CLONE_NEWPID |
process-ID table | PID 1, invisible host processes |
CLONE_NEWNS (mnt) |
mount table | root / — see Filesystems — mount and chroot |
CLONE_NEWNET |
network stack | interfaces, IPs, ports — see Networking — virtual interfaces and bridges |
CLONE_NEWUTS |
hostname/domain | hostname |
CLONE_NEWIPC |
shared-memory table | semaphores/shm |
CLONE_NEWUSER |
UID/GID mapping | user IDs (root inside ≠ root outside) |
CLONE_NEWCGROUP |
cgroup hierarchy view | hidden cgroup tree |
At this point the process sees its own little world. But it can still use the whole machine — nothing stops it eating all the RAM. That's Step 5's job. First, the one piece of "what it sees" that needs its own picture: the filesystem.
Step 4 — The private root: mount namespace + chroot

WHAT. Inside the mount namespace we point the process's / at a directory that contains a whole tiny filesystem (a layered image).
WHY a separate step. "Own root filesystem" is what makes a container feel like a machine: it has /bin, /etc, /usr of, say, Alpine or Ubuntu — even though the kernel is still the host's. This is the trick people mistake for "a whole OS." It is not an OS; it is only the files an OS ships, running on the one shared kernel.
PICTURE. The host's real filesystem tree is drawn in blue. A subtree — the container's image — is highlighted in green. A green arrow relabels that subtree's top as / for the container only. From inside, the blue part above is simply gone from view: the mount table has no entry for it, so it cannot be named.
Step 5 — Cap what it can USE: cgroups and the CPU-fraction formula

WHAT. We drop our now-isolated process into a cgroup (control group) and attach a controller that limits a resource. Take CPU.
WHY a new mechanism. Namespaces changed visibility only — they never touched how much a process may consume. A process with perfect namespaces can still spin up threads and hog every core, starving its neighbours. Limiting quantity is a different job, so the kernel uses a different tool: cgroups, which sit on top of the scheduler (Scheduling — CFS and CPU shares).
PICTURE — the leaky-bucket of CPU time. The cpu.max knob holds two numbers, both in microseconds:
Every period µs the kernel refills your bucket with quota µs of runtime. Spend it all and you are frozen until the next refill. Averaged over time you therefore get:
quota— microseconds of CPU you're allowed per window.period— how long each window lasts.- the ratio — the fraction of one core you average, because runtime given ÷ time elapsed = fraction busy.
The figure shows time on the x-axis split into period-long windows. Within each window a green bar of width quota is your allowed run-time; after it, a red "frozen" bar until the next window. The dashed line is the long-run average — the fraction .
Other controllers work the same way — a number you write, a limit the kernel enforces:
| Controller | Knob | Effect |
|---|---|---|
| memory | memory.max = 268435456 |
OOM-kill past 256 MiB |
| pids | pids.max = 100 |
blocks fork bombs |
| io | io.max |
throttles disk bandwidth |
Step 6 — Degenerate cases (so you never hit a surprise)

Every real setup is a combination of "has namespaces?" and "has cgroups?". The figure is a 2×2 grid — cover all four corners:
Case A — no namespaces, no cgroup (a plain process). Sees everything, uses anything. Not a container. This is Step 1.
Case B — cgroup only, no namespaces. It is limited (can't hog RAM) but not isolated — it can still ls and kill host processes because its PID/mount/net tables are the shared ones.
Case C — namespaces only, no cgroup. It is isolated (its own view) but unlimited — it can fork-bomb the whole machine because nothing caps it. Isolation without limits is a container that can DoS its host.
Case D — both (a real container). Private view and capped usage. This is the goal.
The root-inside edge case. Inside the PID/mount world, whoami may print root (UID 0). Does that mean host-root?
Step 7 — Container vs VM: count the kernels

WHAT. Put our finished container next to a virtual machine and look at the one structural difference.
WHY it matters. Everything about weight, speed, and security follows from a single fact: how many kernels are running.
PICTURE — side by side.
- VM (left). A hypervisor emulates hardware (see Virtualization and Hypervisors (Type 1 vs Type 2)); on top of it each VM boots a whole guest kernel + OS. Two boxes → two kernels. Strong walls (hardware-enforced), but GBs and seconds to boot.
- Container (right). Our process group shares the one host kernel; isolation is the namespaces+cgroups we just built. One kernel. MBs and milliseconds, but a kernel bug is a shared blast radius.
| Container | VM | |
|---|---|---|
| Kernels running | one (shared host) | two+ (own guest) |
| Isolation by | namespaces + cgroups | hypervisor + virtual HW |
| Boot | ms | seconds |
| Size | MBs | GBs |
| Security wall | curtains (software) | concrete (hardware) |
| Run a foreign kernel? | ❌ | ✅ |
The one-picture summary

The final figure stacks the whole derivation into one diagram: at the bottom, the single host kernel (Step 1); above it, our process wrapped in seven namespace curtains (Steps 2–4) giving it a private view; around it a cgroup meter (Step 5) capping CPU/RAM; and to the side, the VM with its extra kernel for contrast (Step 7). That stack is a container.
Recall Feynman retelling — the whole walkthrough in plain words
Start with one kid (a process) standing in a shared house (the kernel), able to see and use every room. To make a container we do two separate jobs. First, curtains (namespaces): we hand the kid private copies of each "who's here / where's the kitchen / what's the address" list — PIDs, filesystem root, network, hostname, and so on — so they can only see their own little corner; in their own list they become "kid number 1." Then a chore chart (cgroups): we say "you may use half a TV" — quota ÷ period of one core — and the kernel freezes them once they've used their share this window. Curtains alone means they could still fork-bomb; a chart alone means they still see everyone — you need both. And through all of this it's still the same house, same kitchen: one kernel. A virtual machine, by contrast, builds a whole second house next door — its own kernel — which is why it's heavy and slow but has real concrete walls instead of curtains.
Flashcards
In the isolation rule, when is a process "fully isolated"?
CLONE_NEW* flags together