A container looks, from the outside, like a tiny virtual machine: it has its own filesystem, its own process list, its own network interface, and starting one feels like booting something. None of that is true underneath. A container is a single, ordinary process running directly on the host’s kernel — the same kernel every other process on that machine uses — made to believe it’s alone on the machine through two separate mechanisms. Namespaces control what that process can see. cgroups control what it can use. Everything that makes a container feel like an isolated machine is one of those two things, and nothing else.
The Core Claim: No Second Kernel
The architecture of Linux article drew the line between user space and kernel space: every process, no matter how it’s packaged, ultimately makes system calls into one kernel running on the hardware. A virtual machine hides that line behind a hypervisor and a second, fully separate kernel — a guest OS with its own process table, its own view of memory, its own drivers, running on virtualized hardware. A container does not do this. docker ps on the host and ps inside a container are, underneath, looking at the exact same kernel data structure, filtered differently. This is why containers start in milliseconds where VMs take seconds to boot an entire operating system, and why a container’s resource overhead is close to zero beyond what its own process actually uses — there’s no second kernel being run twice, only one kernel presenting a restricted view to a process that doesn’t know it’s restricted.
Namespaces: Six Ways to Not See Something
A namespace wraps one specific kind of global kernel resource and gives a process its own private instance of it, isolated from every other namespace of the same kind. Linux has several kinds, each independent of the others.
PID namespace gives a process its own view of the process tree. Inside a container, the first process is PID 1 — as far as that namespace is concerned, nothing else exists or ever existed. The processes and signals article described PID 1’s special role as the ultimate parent that reaps orphaned children; a container’s PID 1 inherits exactly that responsibility within its own namespace, which is why a container image that runs an application directly as PID 1 without a proper init process can leak zombies in exactly the way that article warned about — there’s no real init underneath to reap them, only whatever the container’s own entrypoint happens to do.
Mount namespace gives a process its own view of the filesystem tree — its own root, its own set of mounted volumes — so that what looks like / inside the container can be a completely different directory (or an entirely synthetic layered filesystem, below) than what / means on the host, with neither side able to see the other’s mounts.
Network namespace gives a process its own network stack: its own interfaces, its own routing table, its own iptables rules, separate from the host’s. A container’s eth0 is almost always one end of a virtual ethernet pair, with the other end wired into the host’s network namespace — the same veth mechanism the iptables and netfilter article touched on for routing traffic between isolated contexts.
UTS namespace isolates the hostname, letting a container report its own hostname via hostname without that name meaning anything to the host or any other container. IPC namespace isolates System V IPC objects and POSIX message queues, so two containers can’t accidentally signal each other through a shared memory segment that happens to share an ID. User namespace maps UIDs inside the container to a different (often unprivileged) range of UIDs on the host, so a process that is root inside the container can be an ordinary, unprivileged user on the host — the single most important namespace for containing what happens if the isolation is ever broken out of.
A container runtime creates a new process with most or all of these namespaces active at once, via the clone() system call with a set of CLONE_NEW* flags, or the standalone unshare() call to peel a running process into new namespaces after the fact. Either way, the process itself runs completely unmodified — it’s an ordinary binary making ordinary system calls. It’s the kernel’s bookkeeping behind those calls that’s different.
cgroups: What the Process Is Allowed to Use
Namespaces answer “what can this process see.” They say nothing about how much CPU it can burn or how much memory it can allocate — a process with its own private view of the world can still starve every other process on the host for real, shared resources. That’s a separate kernel mechanism: control groups, or cgroups.
A cgroup is a mechanism for grouping processes and applying resource limits and accounting to the group as a whole, enforced directly by the kernel’s scheduler and memory manager rather than by anything the process itself cooperates with. The cron and systemd timers article already touched these directives from the other direction, pointing out that MemoryMax= and CPUQuota= on a systemd service apply through “the same cgroup-based directives as any service” — a systemd service and a container are, at this layer, doing the exact same thing: handing a process (or a tree of them) to the kernel’s cgroup controller along with a ceiling it’s not allowed to cross.
memory limits how much RAM (and swap) the group can allocate in total; exceed it and the kernel’s out-of-memory killer terminates a process in the group rather than letting the host run out of memory for everyone. cpu limits how much CPU time the group gets, either as a hard quota (a fixed number of CPU-microseconds per period) or a relative share against other groups when the CPU is contended but not maxed out. pids caps how many processes or threads a group can spawn in total — the cgroup-level defense against the fork-bomb scenario where a single container, PID-namespaced into believing it’s alone, forks itself into exhausting the host’s actual process table. blkio throttles disk I/O bandwidth per group, so one noisy container can’t starve the actual disk for every other tenant on the same host.
Where a namespace is a private view, a cgroup is a shared ceiling — and a container needs both for its two separate guarantees. Without namespaces, a contained process could see and signal every other process on the host. Without cgroups, a contained process could see only itself, happily unaware that it had just consumed every byte of RAM on the machine.
What docker run Actually Does
Put the pieces together and a container runtime’s real job is almost mechanical: create a new process in a fresh set of namespaces, place that process into a cgroup with the limits the user asked for, change its filesystem root to an unpacked container image via pivot_root, and then exec() the actual entrypoint binary inside that prepared environment. Docker, containerd, and runc all implement this same sequence — runc is literally the reference implementation of the OCI runtime spec that formalizes these exact steps — and the “container” that results is nothing but a process tree the kernel is tracking with a specific combination of namespace and cgroup metadata attached to it.
The filesystem piece deserves its own note: a container image is a stack of read-only layers (a base OS layer, then a layer per build step) merged at runtime by a union filesystem like overlayfs, with one thin writable layer on top for anything the running container changes. This is why starting a container is fast — there’s no copying, just a new overlay mount pointing at layers that already exist on disk, usually shared across every container started from the same image — and why a container’s filesystem changes vanish when it’s removed: they only ever lived in that one disposable top layer.
The Boundary That Matters
A VM’s isolation boundary is the hypervisor and a second kernel; breaking out of it means finding a bug in virtualized hardware emulation, which is rare and expensive to find. A container’s isolation boundary is a single shared kernel enforcing namespace and cgroup rules on an otherwise ordinary process; breaking out of it means finding a bug in the kernel itself, or a misconfiguration that handed the container more than it should have had — a mounted host directory it shouldn’t be able to write to, a capability it shouldn’t have been granted, a user namespace left unmapped so root-in-container really is root-on-host. That’s a smaller attack surface in absolute terms, because namespace and cgroup enforcement is simpler code than full hardware virtualization, but it’s also a single point of failure shared by every container on the host, in a way a hypervisor’s per-guest kernels are not. Understanding containers as “processes with a kernel-enforced disguise,” rather than “small VMs,” is what makes that trade-off visible instead of assumed.