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:

text
   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:

text
   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 host

Put 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:

text
   cgroup: web-app
     memory.max   = 512M     → OOM-killed if exceeded
     cpu.max      = 50%      → throttled past its share
     pids.max     = 100

This 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.

bash
# A crude container by hand: new namespaces + a different root
sudo unshare --pid --net --mount --uts --fork --mount-proc \
    chroot /path/to/rootfs /bin/sh

Why 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.