Containers vs Virtual Machines
Containers and virtual machines both let you run software in an isolated, repeatable box, but they do it in very different ways. A virtual machine (VM) pretends to be a whole separate computer, complete with its own operating system. A container shares the host computer’s operating system kernel and only packages your app and its libraries. Knowing which one to reach for is one of the most important judgment calls in DevOps, so this page breaks down exactly how they differ and when each one wins.
The core difference: kernel sharing vs hardware virtualization
The single idea that explains everything else is this: VMs virtualize hardware, containers share the kernel.
The kernel is the core part of an operating system (the program that talks directly to your CPU, memory, and disk). When you run a VM, a piece of software called a hypervisor (the layer that creates and runs virtual machines, such as KVM on Linux or VMware) hands each VM a fake set of hardware. Every VM then boots its own full operating system on top of that fake hardware — its own kernel, its own system services, the works.
A container does none of that. It runs as an ordinary process on the host, and it borrows the host’s already-running kernel. The container engine (Docker, for example) just uses two built-in Linux features — namespaces (which give a process its own private view of things like processes, network, and filesystem) and cgroups (which limit how much CPU and memory the process can use) — to wall the container off. There is no second operating system underneath. That is why a container is so much lighter.
A picture in words
Think of a single Ubuntu server.
- With VMs, on top of that server you stack a hypervisor, and on top of the hypervisor you stack three full guest operating systems, and inside each one runs one app. Three kernels, three sets of system services.
- With containers, on top of that server you stack one container engine, and on top of it you run three lightweight containers that all share the host’s single kernel. One kernel, three apps.
Side-by-side comparison
| Property | Containers | Virtual machines |
|---|---|---|
| Boot / start time | Milliseconds to a second | Tens of seconds to minutes |
| Size on disk | Tens to hundreds of MB | Several GB (full OS image) |
| Overhead (CPU/RAM) | Very low — just your process | High — a whole guest OS per VM |
| Density (how many fit on one host) | Hundreds on a modest server | A handful, depending on RAM |
| Isolation strength | Process-level (shared kernel) | Strong, hardware-level |
| OS sharing | Shares the host kernel | Each VM has its own kernel/OS |
| Can run a different kernel/OS | No (Linux container needs Linux host) | Yes (run Windows on Linux, etc.) |
The headline: containers are faster, smaller, and far denser. VMs give you stronger isolation and the freedom to run a completely different operating system.
See it for yourself on Ubuntu
You can feel the speed difference with one command. First make sure Docker is installed (covered in Installing Docker), then time how long a container takes to start.
time docker run --rm ubuntu:24.04 echo "hello from a container"
Output:
hello from a container
real 0m0.642s
user 0m0.031s
sys 0m0.028s
Under a second, including pulling the image into memory on a warm cache. Booting a full Ubuntu VM to print the same line would take 20-40 seconds.
You can also prove that a container is sharing your host kernel. Compare the kernel version inside a container with the one on the host:
uname -r
docker run --rm ubuntu:24.04 uname -r
Output:
6.8.0-45-generic
6.8.0-45-generic
They are identical. The container did not boot its own kernel — it used yours. A VM would report a different kernel that it booted itself.
When you still want a VM
Containers are the default for most modern app deployment, but VMs are far from dead. Reach for a VM when:
- You need a different operating system or kernel. A Linux container cannot run a Windows-only program, and it cannot use a kernel feature your host kernel lacks. A VM can boot any OS.
- You need strong, hardware-enforced isolation. Because containers share one kernel, a kernel-level security flaw could in theory let one container affect others. For untrusted code or strict multi-tenant security (different customers on the same box), the harder VM boundary is safer.
- You are running the host for containers in the cloud. Almost every cloud container service actually runs your containers inside VMs you do not see, getting the best of both: VM isolation between tenants, container speed within them.
- You need to run a full virtual desktop or legacy appliance that expects to control its own operating system.
Container isolation is good but not airtight. Never run untrusted code as the
rootuser inside a container, and never pass--privilegedtodocker rununless you fully understand it — that flag tears down most of the isolation and effectively gives the container root on your host.
A common and powerful pattern is to combine them: use VMs to carve a big physical server into a few isolated hosts, then pack many containers onto each VM.
Best practices
- Default to containers for deploying your own application code — they are faster, cheaper, and easier to ship.
- Choose VMs when you need a different OS/kernel, stronger isolation, or must run untrusted multi-tenant workloads.
- Treat the shared kernel as both the superpower (low overhead) and the limitation (weaker isolation) of containers, and design security around it.
- Keep host kernels patched with
sudo apt update && sudo apt upgrade; because every container shares it, one kernel fix protects all of them at once. - Run containers as a non-root user and avoid
--privilegedto keep the isolation you do have. - Don’t try to put a full operating system “inside” a container — a container should hold one app, not a whole machine.