Docker & ContainersI · FoundationsContainers vs VMs
Containers vs virtual machines
What you'll learn
- Distinguish a container from a VM at every layer of the stack
- Choose the right primitive for a given workload
- Recognise where containers do and do not provide isolation
Prerequisites
None — start here.
Verified against Docker Engine 29.x · Docker Engine 28.x · Docker Compose 2.x · containerd 2.x · runc 1.2.x · BuildKit 0.20+ · Linux kernel 5.15+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-09
The phrase “Docker is just a process” is the most useful sentence in this entire course. It is also the source of every misunderstanding sysadmins have about containers. A container is a Linux process, with a few extra kernel features wrapped around it. A VM is an entire hardware emulation. The two have almost nothing in common.
This lesson sets up the mental model that every later lesson assumes.
What a virtual machine actually is
A VM is hardware presented to a guest operating system. The host kernel exposes a virtual CPU, virtual RAM, virtual disk, and virtual NICs. The guest kernel boots inside that hardware, then runs userspace processes. To the guest, the world looks like real hardware — it doesn’t know it’s a VM.
A container, by contrast, is just a process tree running on the host kernel. The host kernel creates the illusion of isolation through namespaces (so the process sees its own PIDs, mounts, network, etc.) and cgroups (so the process is constrained in its use of CPU, RAM, and I/O). The container’s “operating system” is the host’s kernel.
flowchart TB
subgraph VM["Virtual Machine"]
direction TB
AppA1[App A]
AppA2[App B]
LibA[libs / runtime]
KernelA[Guest Kernel]
HyperA[Hypervisor]
end
subgraph Container["Container"]
direction TB
AppB1[App A]
AppB2[App B]
LibB[libs / runtime]
NoKernel[No kernel — uses host kernel]
end
subgraph Host["Host kernel + hardware"]
HostKernel[Host kernel]
HW[CPU, RAM, disk, NIC]
end
VM --> HostKernel --> HW
Container --> HostKernel --> HW
A side-by-side comparison
| Aspect | Virtual machine | Container |
|---|---|---|
| Boot time | Seconds to minutes | Milliseconds (just fork+exec) |
| Disk footprint | Gigabytes (full guest OS) | Tens to hundreds of MB (app + libs) |
| RAM overhead | Hundreds of MB to GB (guest kernel) | Single-digit MB (process overhead) |
| Isolation | Strong (separate kernel) | Weaker (shared kernel) |
| Density | Tens per host | Hundreds per host |
| Image portability | Hypervisor-bound; less portable | OCI image; portable across distros |
| Kernel features | Whatever the guest supports | Whatever the host supports |
Where the boundary actually is
The kernel features that separate containers from “just a process” are:
- Namespaces — give the process its own view of PIDs, mounts, the network stack, UTS (hostname), IPC, and (with user namespaces) users. Lesson “Linux namespaces” covers this in depth.
- cgroups — limit CPU, memory, PIDs, block I/O, and (in cgroups v2) several other resources. The kernel enforces the limits; if the container exceeds its memory limit, the kernel OOM-kills it.
- Filesystem isolation — the root filesystem is built from image layers (OverlayFS). Bind mounts and volumes punch holes through it.
- Capability dropping — Linux capabilities are a fine-grained
replacement for “root or not root”. A container can drop everything
except
CAP_NET_BIND_SERVICE, for example. - Seccomp — restricts which syscalls a process can make. The default Docker seccomp profile blocks ~50 dangerous syscalls.
- AppArmor / SELinux — MAC profiles that further restrict what a process can do.
A container does not have its own kernel. A VM does. The choice is between “lots of processes, one kernel” (container) and “fewer guest kernels, each with their own kernel” (VM).
Portable image, non-portable kernel
The image is portable. The kernel it needs is not, and nothing in the image records that dependency. This is the practical edge of “shares the host kernel”, and it is where the container/VM distinction stops being an architecture-diagram fact and starts costing a night.
IMAGE=ubuntu:24.04
docker run --rm "$IMAGE" sh -c 'grep ^PRETTY_NAME /etc/os-release; uname -sr'
echo '--- host ---'
grep ^PRETTY_NAME /etc/os-release; uname -srPRETTY_NAME="Ubuntu 24.04.3 LTS"
Linux 5.14.0-427.el9.x86_64
--- host ---
PRETTY_NAME="Rocky Linux 9.4 (Blue Onyx)"
Linux 5.14.0-427.el9.x86_64Illustrative output
The container reports Ubuntu 24.04 and a Rocky Linux kernel, because
that is exactly what it is: Ubuntu’s userspace on the host’s kernel.
The two uname lines being identical is the whole point of the
exercise — there is no second kernel anywhere in this picture, and
any check that concludes “the environment matches” from
/etc/os-release alone has checked the half that was never in
doubt.
This is also the test that disproves the “it’s a lightweight VM” model in one command, which is worth doing early with anyone who still holds it.
echo "kernel: $(uname -r)"
echo "vm.max_map_count: $(sysctl -n vm.max_map_count)"
echo "cgroup version: $(stat -fc %T /sys/fs/cgroup)"
echo "overlay module: $(grep -c '^overlay ' /proc/modules)"
echo "unprivileged_userns: $(sysctl -n kernel.unprivileged_userns_clone 2>/dev/null || echo 'n/a on this kernel')"kernel: 6.8.0-79-generic
vm.max_map_count: 262144
cgroup version: cgroup2fs
overlay module: 1
unprivileged_userns: n/a on this kernelIllustrative output
cgroup2fs confirms the unified hierarchy; tmpfs there means
cgroup v1 and a different set of resource-limit behaviours. Capture
this for every host class you run on and keep it next to the image
requirements — that comparison is the thing a container image cannot
do for you and a VM image would not have needed.
What a container is NOT
A container is not:
- A lightweight VM. There is no guest kernel, no virtual hardware.
- A sandbox by default. The default seccomp profile is a baseline; the default capability set is broad. You must harden.
- A magic security boundary. A privileged container is roughly as powerful as root on the host.
- An alternative to backups. Container filesystems are ephemeral by design. Persistent data lives in volumes or external storage.
- An orchestration tool. Docker Compose describes multi-container apps; it does not schedule them across hosts. That requires Swarm, Kubernetes, or an external orchestrator.
When to choose what
- Use containers when you want fast startup, high density, portable images, and CI/CD-driven deployment. Linux workloads, microservices, batch jobs.
- Use VMs when you need a different kernel (Windows, BSD), a hard security boundary between workloads (multi-tenant), or per-workload kernel configuration that conflicts with the host.
- Use both when you have a fleet of VMs as the host substrate and run containers inside them. This is the most common production pattern.
- Use bare metal when neither container nor VM overhead is acceptable, or when the workload requires direct hardware access (GPU passthrough, DPDK, raw NVMe).
Common misconceptions
“Containers are more secure than VMs because they’re isolated.”
Containers share a kernel with the host. The kernel is the single largest attack surface. A kernel-level exploit inside one container affects every container on the host. VMs do not share a kernel.
“Containers start in milliseconds because they’re lighter.”
They start in milliseconds because there’s nothing to start. The init
process is exec’d directly. There is no firmware, no bootloader, no
kernel init, no userspace init system to skip. The “weight” being
saved is the entire boot process.
“If I run my container with
--memory 512m, the host is safe from OOM.”
The cgroup limits the container, but a misconfigured cgroup (or a
cgroup-less OOM-killer bypass) can still cause issues. Plus, the host
kernel’s OOM-killer chooses which process to kill based on
oom_score, and your container’s processes may not get the lowest
score. Always test OOM behaviour, don’t assume it.
Knowledge check
Knowledge check · 5 questions
Q1. Which of the following is true of a container but NOT of a virtual machine?
Q2. A privileged Docker container running as root is roughly equivalent to root on the host.
Q3. Name two Linux kernel features that together provide container isolation.
Q4. A workload needs vm.max_map_count=262144. `docker run --sysctl vm.max_map_count=262144` is rejected by the daemon. Why, and what do you do?
Q5. Running `uname -r` inside an ubuntu:24.04 container tells you which kernel that container is running.
Passing score: 75%. Answers are checked in this browser.
Where next
The next lesson, “The OCI ecosystem,” introduces the standards bodies that define the image and runtime formats. After that, “Docker architecture at a glance” walks through every daemon, runtime, and shim that participate in the lifecycle.