07 · Containers vs VMs: Where OS Fundamentals End and Virtualization Begins¶
Level 3 built a working, isolated, resource-limited process using nothing but unshare and a cgroup — the real
mechanism behind containers. This entire course was also written and verified inside a real virtual machine. Both
are "isolation," and the gap between them is the single most useful distinction in this whole level for deciding
which tool actually fits a given problem.
The real, measured difference¶
$ systemd-analyze # this course's lab VM, full boot
Startup finished in 317ms (kernel) + 1.336s (initrd) + 2min 45.052s (userspace)
^^^^^^^^^^^^^^^^^^^^^^^^
(mostly cloud-init network wait,
Level 1 Lesson 2 — but even without
that, a real kernel boot, firmware
init, and full userspace startup
are unavoidable real costs)
$ time sudo unshare --pid --mount-proc --fork true # Level 3's namespace demo
real 0m0.013s
user 0m0.003s
sys 0m0.009s
A container (what unshare+cgroups built directly, Level 3 Lessons 7 and 10) is a normal process on the
same running kernel, made to see and consume only a restricted view of that kernel's resources. There is
exactly one kernel involved, one real boot already happened (long before the container existed), and creating a
new container is just creating a new process with some extra kernel bookkeeping (new namespaces, a cgroup
assignment) — which is why it completes in milliseconds, as measured directly above. A virtual machine boots
an entirely separate kernel on top of emulated or paravirtualized hardware, provided by a hypervisor (this
course's lab VM uses Apple's vz framework) — a genuinely separate operating system instance, with its own real
(if virtual) boot sequence, which is why even a fast VM boot measures in seconds to minutes, not milliseconds,
no matter how lightweight the guest OS is.
How It Actually Works¶
This course's own Level 1 and Level 3 projects drew this exact line in practice, even if it wasn't named that
way at the time: chroot (Level 1) changed only the filesystem root view and shared literally everything else,
including the kernel, with the host — provably, since uname -a inside and outside the chroot reported the
identical kernel build. unshare (Level 3) added process-tree and hostname isolation on top of that same shared
kernel — still one kernel, more of it hidden from view. Neither of these ever involved a second kernel booting;
every layer of isolation this course built was isolation within one running kernel instance. A VM is
categorically different: the hypervisor presents virtual (or paravirtualized, like this lab's virtio_blk disk
from Level 4, Lesson 3) hardware to a guest kernel that boots independently, goes through its own BIOS/UEFI
handoff, its own init, its own process table that the host kernel knows nothing about directly — real isolation
at the hardware-emulation boundary, not at the single-kernel-resource-view boundary containers use.
This is exactly why a kernel vulnerability that lets code escape a container's namespace/cgroup boundary is historically more common and more severe than one that escapes a VM's hypervisor boundary: a container escape lands you directly in the one shared kernel everything was always running on, with no second security boundary behind it; a VM escape (a "hypervisor breakout") has to defeat hardware-assisted virtualization isolation specifically designed to resist exactly that, which is a substantially harder and rarer class of bug. This isn't a reason to consider containers insecure by default — properly configured, with LSM profiles, seccomp, and non-root user namespaces (all mentioned as missing from this course's own minimal demos), containers are used safely at enormous scale — but it is the real, structural reason "a container is basically a lightweight VM" undersells a genuine difference in what's actually providing the isolation.
Common mistakes¶
- Choosing a VM "to be safe" for a workload where container isolation (correctly configured, with the pieces this course's demos deliberately left out for simplicity) would be entirely sufficient, paying real boot-time and resource overhead for a security margin the actual threat model may not need.
- Choosing a container for a workload that genuinely needs a different kernel, different kernel-level security boundary, or hardware-level isolation (multi-tenant untrusted code execution, for instance) and discovering the shared-kernel assumption the hard way.
- Assuming "container" means one fixed thing. As Level 3 showed directly, it's a combination of independently-tunable kernel mechanisms — a container with every available isolation feature enabled is a very different security posture from one with the bare minimum, even though both are correctly called "a container."
Exercise¶
Time unshare --pid --fork true (or similar) against booting any VM you have access to, even one with a minimal
guest OS, and compare the two numbers directly, as this lesson did. Then explain, in your own words, using the
"what is actually providing the isolation" model from this lesson, why the two numbers differ by roughly two to
four orders of magnitude rather than by some small constant factor.