The purpose of containerisation (Docker) and VMs is to isolate an application and its dependencies from it’s environment so as to avoid dependency conflicts.
How Docker Isolates Processes
The three primitives Docker combines:
chroot + namespaces + cgroups = a container
- chroot / pivot_root — points the process at a different root filesystem (the image)
- namespaces — controls what the process can see (its own PID tree, network stack, mounts, hostname)
- cgroups — controls how much it can use (CPU, memory, I/O)
The result is a normal Linux process that makes normal system calls, is scheduled by the real kernel scheduler, and shares the host’s kernel. There is no second kernel anywhere.
Docker does not virtualise hardware. Docker does not virtualise a kernel. It asks the kernel to present a filtered view of itself to a process.
How a VM Isolates Processes
A virtual machine takes a completely different approach. Instead of asking the kernel for isolation, it replaces the kernel entirely — for each VM.
A hypervisor (KVM, VMware, Hyper-V) runs on the host and presents each VM with emulated hardware: virtual CPUs, virtual RAM, virtual disks, virtual NICs. The VM boots a full OS on top of that virtual hardware, completely unaware it’s not running on real metal.
your application
↓
guest userspace (libc, runtime, etc.)
↓
guest kernel (its own Linux, Windows, etc.)
↓
hypervisor (virtualises hardware)
↓
host kernel
↓
real hardware
Everything above the hypervisor is fully duplicated per VM.
Side-by-Side Comparison
| Container | Virtual Machine | |
|---|---|---|
| Kernel | Shared with host | Each VM has its own |
| OS | Host OS only | Full guest OS per VM |
| Isolation mechanism | Namespace + cgroup (kernel features) | Hardware virtualisation (hypervisor) |
| Boot time | Milliseconds (just a process) | Seconds to minutes (full OS boot) |
| Image size | Megabytes (userspace only) | Gigabytes (full OS + kernel) |
| Memory overhead | Minimal (no guest kernel) | Significant (each guest OS uses RAM) |
| Density | Hundreds per host is normal | Tens per host is typical |
| Syscall path | Direct to host kernel | Guest kernel → hypervisor → host kernel |
| Kernel compatibility | Must match host kernel ABI | Any kernel, any OS |
| Security boundary | Namespace escape = host access | VM escape is extremely difficult |
The Security Trade-off
This is the most important practical difference.
A container shares the host kernel. If an attacker exploits a kernel vulnerability from inside a container, they are already talking to the real kernel — the only barrier is namespaces and capabilities, which are kernel features that can themselves have bugs.
A VM has a hard hardware boundary. Even if an attacker fully compromises the guest kernel, they still face the hypervisor to reach the host. VM escapes exist but are rare and difficult.
Container threat model:
attacker → container process → kernel exploit → host kernel ✓ (same kernel)
VM threat model:
attacker → guest process → guest kernel exploit → hypervisor exploit → host kernel
(extra barrier)
This is why security-sensitive multi-tenant workloads (cloud providers running untrusted customer code) typically use VMs, not containers — or use purpose-built lightweight VMs like Firecracker or gVisor that combine VM-level isolation with container-like startup speed.
What Each is Good For
Use containers when:
- You control the workload — your own app, your own team, trusted code
- You want speed — fast startup, fast iteration, fast CI pipelines
- You want density — many small services on one machine
- You want consistency — same image from dev laptop to production
Use VMs when:
- You need strong isolation — running untrusted or multi-tenant workloads
- You need a different OS or kernel — Windows on Linux, or a specific kernel version
- You need hardware-level guarantees — compliance, regulated environments
- You need full OS features — a real init system, kernel modules, hardware drivers
In Practice, You Often Use Both
Most production cloud infrastructure layers VMs and containers together:
Physical host
└── VM (hardware isolation boundary)
└── Container runtime (Docker / containerd)
├── Container A
├── Container B
└── Container C
The VM provides isolation between tenants or environments. Containers provide density, speed, and consistency within that VM. This is exactly how AWS ECS, Google Cloud Run, and most Kubernetes deployments work.