People picture a container as a tiny virtual machine. It isn’t. There’s no guest OS, no hypervisor, no emulated hardware. A container is an ordinary process on the host kernel that has been lied to about what it can see and limited in what it can use. Two kernel mechanisms do all the work: namespaces and cgroups.
A Container Is Just a Process
Run a container and look on the host — you’ll find its processes in the normal process list, scheduled by the normal scheduler, sharing the host kernel:
VM: Container:
┌─────────────┐ ┌─────────────┐
│ guest kernel│ │ process │
├─────────────┤ └──────┬──────┘
│ hypervisor │ │ syscalls
├─────────────┤ ┌──────▼──────┐
│ host kernel │ │ host kernel │ ← shared, no guest
└─────────────┘ └─────────────┘That shared kernel is why containers start in milliseconds and cost almost nothing — and also why isolation is weaker than a VM’s.
Namespaces: Lying About What Exists
A namespace gives a process its own private view of one kind of global resource. The kernel has several:
pid → its own process tree; PID 1 inside is some PID on the host
mnt → its own filesystem mounts (the "container's filesystem")
net → its own interfaces, routes, ports
uts → its own hostname
ipc → its own shared memory / semaphores
user → its own UID range; root inside ≠ root on hostPut a process in fresh namespaces and it sees a pristine system: PID 1, its own network stack, its own root filesystem. It believes it’s alone on the machine. Nothing is virtualized — the kernel just filters what the process can perceive.
cgroups: Capping What It Uses
Namespaces control visibility; control groups (cgroups) control consumption. They cap and account for CPU, memory, I/O, and process count:
cgroup: web-app
memory.max = 512M → OOM-killed if exceeded
cpu.max = 50% → throttled past its share
pids.max = 100This is what stops one container from eating the whole host. docker run -m 512m --cpus 0.5 is just writing these cgroup limits for you.
Docker Is the Convenience Layer
Strip it back and a container is: a process started in new namespaces, confined by
cgroups, with its root filesystem swapped (pivot_root) to an unpacked image.
You can build one by hand with unshare and cgcreate. Docker/containerd add
the image format, layered filesystem, networking setup, and registry — enormous
convenience, but not the isolation itself.
# A crude container by hand: new namespaces + a different root
sudo unshare --pid --net --mount --uts --fork --mount-proc \
chroot /path/to/rootfs /bin/shWhy It Matters
Because the kernel is shared, a kernel exploit from inside a container can reach the host — unlike a VM, where you’d also have to break the hypervisor. That’s the core security tradeoff, and the reason sandboxes like gVisor and Kata exist to put some of the VM boundary back.
Takeaways
- A container is a host process, not a VM — same kernel, no guest OS, hence the millisecond startup.
- Namespaces give it a private view of PIDs, mounts, network, etc.; it only thinks it’s alone.
- cgroups cap its CPU, memory, and I/O so it can’t starve the host.
- Shared kernel means weaker isolation than a VM — the tradeoff behind container security tooling.