BitBootcampBitBootcamp ← all explainers 🔄 Review ⚙ Settings

DEVOPS-02-001-P · Primer — Containers vs VMs

The idea in one sentence

A container shares the host machine's kernel and packages only the app plus its dependencies, while a VM virtualizes hardware and boots a full guest OS on top of a hypervisor — trading isolation strength for startup speed and density.

Why it exists

Picture a hotel built as separate deeded houses (VMs) versus a hotel built as locked rooms sharing one building's plumbing, wiring, and foundation (containers): the room is cheaper to build and faster to move into, but a fire in the shared electrical system can touch every room, where a fire in one house only ever touches that house. Containers exist because most workloads aren't fighting for hardware-grade tenant isolation, they're fighting server sprawl: teams that provisioned one VM per microservice were burning gigabytes of RAM and tens of seconds of boot time on a guest OS nobody needed before a single line of app code ran — and paging on-call for a "slow autoscaler" that was really just booting kernels one at a time.

How it works

A container is a set of Linux namespaces (PID, network, mount, UID) plus a cgroup limiting one process tree's view of, and claim on, a single kernel that's already running — the container engine (containerd, CRI-O) just draws a wall around a slice of the host OS. A VM instead runs a hypervisor (KVM, Xen, Hyper-V) that emulates virtual hardware, and on top of that virtual hardware a full guest kernel boots from scratch — its own scheduler, its own memory manager, its own drivers — with zero visibility into the host kernel. That's why a VM's isolation is a hardware boundary and a container's isolation is a set of policies enforced by a kernel every other container on the box also trusts.

Run the numbers on a real node: 64 GB of RAM, and each unit of your app needs 512 MB of working memory. A VM pays a fixed per-instance guest-OS tax — call it 512 MB for kernel, init system, and OS caches — so each VM instance actually costs \(512+512=1024\) MB, and the node fits \(\lfloor 64\times1024/1024\rfloor=64\) VMs. A container pays a thin per-instance tax, ballpark 5 MB for namespace and cgroup bookkeeping, so each container costs \(512+5=517\) MB and the same node fits \(\lfloor 65536/517\rfloor=126\) containers — nearly double the density from identical hardware, purely because nobody has to boot 126 separate kernels.

The other number that matters under load is time-to-ready. A container starts by forking a process inside an already-running kernel — under a second in practice. A VM boots a kernel, an init system, and services before your app even starts — tens of seconds. That gap compounds under an HPA scale-out: if the scheduler needs \(N\) new replicas at concurrency (maxSurge) \(c\), total time to fully ready is \[T = \lceil N/c \rceil \times t_{startup}.\] Scaling out 20 replicas at \(c=4\) takes \(\lceil20/4\rceil\times1\text{s}=5\text{s}\) with containers but \(\lceil20/4\rceil\times30\text{s}=150\text{s}\) with VMs — the difference between an autoscaler that absorbs a traffic spike and one that's still booting after the spike already caused an outage.

See it — Same hardware, two stacks: VMs boot a full guest OS per app; containers share one host kernel through the engine.

flowchart TD
  subgraph VM["Virtual Machines"]
    HW1["Hardware"] --> HV["Hypervisor"]
    HV --> OS1["Guest OS A"]
    HV --> OS2["Guest OS B"]
    OS1 --> A1["App A"]
    OS2 --> A2["App B"]
  end
  subgraph CT["Containers"]
    HW2["Hardware"] --> HOS["Host OS<br/>+ Kernel"]
    HOS --> ENG["Container<br/>Engine"]
    ENG --> C1["App A"]
    ENG --> C2["App B"]
  end

The one gotcha

Containers share the host kernel, so a kernel panic, a cgroup misconfiguration, or a container-breakout exploit can affect every container on that node at once — true hardware isolation (and real VM-escape resistance) only comes from the hypervisor boundary. That's why regulated or multi-tenant workloads sometimes run containers inside lightweight VMs (Firecracker microVMs, Kata Containers, and how EKS/GKE/AKS isolate node pools) to get both the density and a harder isolation wall.

The one-line takeaway

Containers share a kernel; VMs virtualize hardware.

Where you'll meet it

kubectl describe node shows allocatable RAM that kube-scheduler bin-packs pods into using density math like this; kubelet plus containerd/CRI-O run the containers Kubernetes schedules, while EKS/GKE/AKS let you pick node instance types that trade VM overhead for container density. Firecracker microVMs and Kata Containers run each container inside its own lightweight VM for tenants that need the hypervisor's isolation on top of container density. Helm charts set the resource requests that feed this exact math, and k9s shows real-time per-node RAM utilization when it goes wrong.

Named concepts: containers vs vms