Containers vs Virtual Machines

Two technologies both promise to run "someone else's software, isolated, on my machine": the virtual machine and the container. From a distance they look interchangeable — both give you a fresh Linux you can install things into, both are spun up by clouds by the million. But they solve the problem at opposite ends of the stack, and the difference decides your startup time, your density, your overhead, and — most importantly — how strong your isolation really is.

You already know both halves of the answer. A container, from the last lesson, is a process sharing the host kernel through namespaces and cgroups. A virtual machine, from the virtualization module, is a guest operating system running on emulated or hardware-assisted virtual hardware under a hypervisor. Here is the whole distinction in one line: a VM virtualizes the hardware; a container virtualizes the operating-system interface. Everything else — the milliseconds, the megabytes, the attack surface — falls out of that one choice about where the boundary is drawn.

Two stacks, side by side

Line the layers up and the story tells itself. On the left, each VM is a tall tower: on top of the hypervisor sits a complete guest operating system — its own kernel, its own drivers, its own \texttt{init}, its own page cache — and only then your app. On the right, the host kernel is shared; the runtime carves it into isolated views, and each container is just the app and its files. The tall repeated block on the left — the guest kernel — is precisely what the container omits.

Count the kernels. Ten VMs on a host means eleven kernels running (one host — well, the hypervisor — plus ten guests). Ten containers means one kernel, shared. That single fact drives every number in the trade-off table below.

The trade-off, quantified

Because a container skips the guest kernel, it skips guest boot, guest memory, and the hypervisor's world-switch overhead. Because a VM keeps the guest kernel, it keeps a hard hardware-enforced boundary the guest cannot see past. Neither is "better"; they trade the same axis in opposite directions — overhead versus isolation, the theme that has run through this entire course.

DimensionVirtual machineContainer
Virtualizesthe hardwarethe OS interface
Guest kernelyes — a full one per VMnone — shares the host kernel
Startupseconds (full OS boot)milliseconds (just \texttt{exec} a process)
Memory footprinthundreds of MB (kernel + OS baseline)megabytes (only the app)
Density per hosttenshundreds to thousands
Runtime overheada few % (hardware-assisted)near zero — native syscalls
Isolation boundarystrong — hardware-enforced, own kernelweaker — shared kernel = larger attack surface
Kernel choiceany guest OS (even Windows on Linux)host kernel only (Linux on Linux)

Read the last two rows together. A VM's strength (a private guest kernel) is exactly the source of its cost (booting and carrying that kernel); a container's strength (sharing one kernel) is exactly the source of its weakness (one kernel bug can breach every container). The engineering question is never "which is better?" but "for this workload and this threat model, which side of the axis do I want?"

Density and boot, with real numbers

Make it concrete. Suppose a host has 64 GB of RAM. A minimal Linux VM needs a baseline of maybe 256 MB just for its guest kernel and OS before your app uses a byte; a container adds essentially only the app's own footprint, say 20 MB. The simulation computes how many of each fit, and how the boot budget compares.

// Density & boot: how the shared-kernel choice pays off in RAM and startup time. const hostRamMB = 64 * 1024; // 64 GB host // Per-instance memory: a VM carries a whole guest OS; a container carries only the app. const vmBaselineMB = 256; // guest kernel + OS baseline const ctBaselineMB = 20; // just the app's own working set // Per-instance startup: VM boots a kernel; a container just exec()s a process. const vmBootMs = 2500; // full guest boot const ctBootMs = 30; // fork/exec into namespaces const vmFit = Math.floor(hostRamMB / vmBaselineMB); const ctFit = Math.floor(hostRamMB / ctBaselineMB); console.log(`Host RAM: ${hostRamMB} MB\n`); console.log(`VMs that fit: ${vmFit} (@ ${vmBaselineMB} MB each)`); console.log(`Containers that fit: ${ctFit} (@ ${ctBaselineMB} MB each)`); console.log(`=> ${(ctFit / vmFit).toFixed(1)}x higher density with containers\n`); console.log(`Cold start 100 instances:`); console.log(` VMs: ${(100 * vmBootMs / 1000).toFixed(0)} s of boot`); console.log(` Containers: ${(100 * ctBootMs / 1000).toFixed(1)} s`); console.log(`=> containers start ~${Math.round(vmBootMs / ctBootMs)}x faster — this is why serverless loves them`); console.log(`\nThe catch: all ${ctFit} containers trust the SAME kernel. One kernel bug = everyone's problem.`);

The design law

Because clouds run your code next to a stranger's code, and "weaker isolation" stops being an abstraction when the neighbour is hostile. Multi-tenant serverless (AWS Lambda, Fargate) needs container-like millisecond starts and VM-grade isolation — seemingly a contradiction. The answer, which the last lesson of this module explores, is the microVM: a radically stripped hypervisor (Firecracker) that boots a minimal guest kernel in around 125 ms with a tiny device model, giving each function its own real kernel without the multi-second, hundreds-of-MB tax of a general-purpose VM. The industry's verdict was essentially: "containers for packaging and speed, a thin VM for the isolation." You do not always have to pick one end of the axis — sometimes you engineer a new point on it.

The single most common misconception, repeated in a thousand blog posts: "a container is just a lightweight virtual machine." It is not lightweight because it is a small VM — it is lightweight because it is not a VM at all. There is no guest kernel to be light or heavy; there is no virtual hardware; the hypervisor is absent. It is a process. Getting this wrong leads to real errors: people assume a container isolates them from a kernel exploit the way a VM does (it does not), assume they can run a Windows container on a Linux kernel (they cannot — the kernel is shared, so a "Windows container" needs a Windows host or a hidden VM), and assume container escape and VM escape are equally hard (they are not — the container shares the kernel it must escape through). Say it precisely: a container is an isolated process; a VM is an emulated computer.