Containers Share a Kernel and That Is the Whole Story
The difference between a container and a microVM is one sentence long, and almost every isolation decision follows from it.
Disclosure: I run Krova Cloud, which builds Firecracker microVMs, so I work with this boundary every day. That experience is why this post can be specific about where each option fits.
The one sentence version
Containers share the host kernel. Virtual machines do not.
Everything else about isolation, security boundaries, startup time, and multi tenancy follows from that single fact. If you internalise nothing else, internalise that.
What a container actually is
A container is not a thing. There is no container object in Linux. A container is a process, running directly on the host kernel, that has been lied to about its environment using three kernel features.
Namespaces control what a process can see. There are several, and each virtualises one global resource:
| Namespace | Hides |
|---|---|
pid |
Other processes, so your process sees itself as PID 1 |
mnt |
The host filesystem, so you see your image instead |
net |
Host network interfaces, so you get your own |
uts |
Hostname |
ipc |
Shared memory and semaphores |
user |
UID and GID mapping, so root inside can be nobody outside |
cgroup |
The cgroup hierarchy itself |
Control groups limit what a process can use: memory, CPU, I/O bandwidth, process count. This is what your limits.memory in Kubernetes sets, and what kills your container when it goes over.
Capabilities, seccomp, and LSMs limit what a process can do. Capabilities split root's powers into about forty separate permissions. Seccomp filters which syscalls are even reachable. AppArmor or SELinux add mandatory access control on top.
Put those together and you have a process that believes it is alone on a machine. It is not. It is one clone() call away from every other process on the box, and every syscall it makes goes to the same kernel that serves everything else.
You can prove this in ten seconds. Run a container and look at the kernel version:
docker run --rm alpine uname -r
# 6.8.0-45-generic
That is not Alpine's kernel. Alpine does not ship a kernel in that image. It is your host's kernel, and every container on the machine reports the same one.
Why sharing a kernel is a problem
The kernel is a very large C program with a very large attack surface. Linux exposes roughly 350 syscalls, plus ioctls, plus filesystem parsers, plus network protocol handlers.
A container escape is any bug that lets code inside a container reach the host or another container. Historically these have come from a few places: kernel bugs in syscalls reachable from inside the container, bugs in the container runtime itself, and misconfiguration by the operator.
That third category is the biggest in practice, and it is worth being blunt about how easy it is:
# any of these effectively removes the boundary
docker run --privileged ...
docker run -v /var/run/docker.sock:/var/run/docker.sock ...
docker run --pid=host ...
docker run --cap-add=SYS_ADMIN ...
Mounting the Docker socket is the one I see most, usually in CI images that need to build other images. It gives the container full control of the daemon, which means full control of the host. It is not an escape, it is a handover.
The important framing: the container boundary is a configuration, not a wall. It is assembled from a dozen independent kernel features, each of which can be individually weakened, and most of which have a flag that turns them off.
What a microVM changes
A virtual machine boots its own kernel. Your code makes syscalls to that kernel, which is running in a guest, and the only interface to the host is the hypervisor.
The attack surface changes shape completely. Instead of 350 syscalls into a shared kernel, you have a handful of emulated devices. Firecracker in particular exposes a deliberately tiny surface: a virtio block device, a virtio network device, a serial console, a keyboard controller with one key for reset, and a timer. That is close to the entire device model.
The tradeoff historically was weight. A traditional VM boots a full firmware, a bootloader, and a general purpose kernel with device probing, which takes tens of seconds and hundreds of megabytes.
Firecracker removes almost all of that. No BIOS, no PCI enumeration, a minimal device model, and a kernel booted directly. A microVM boots in well under a second and the VMM process itself uses a few megabytes.
That is the actual innovation, and it is why this category exists: VM grade isolation at close to container startup cost. AWS runs Lambda and Fargate on it, which is a reasonable proof that the model works at scale.
Where each one is right
Both tools are good at different jobs, and knowing the line is the whole point of this post.
Use containers when the code is yours. If you wrote everything running on the box, and your threat model is "a bug in my own service", the container boundary is fine. You are using namespaces for packaging and resource limits, not as a security wall, and that is a completely legitimate use. The overwhelming majority of Kubernetes workloads are this, and adding a hypervisor to them buys you very little.
Use containers for extreme density. A thousand small containers on one host is normal. A thousand microVMs each reserve their RAM, which is the trade you make for a real boundary. Reserved memory is also why a microVM tenant gets the capacity they paid for instead of competing for page cache with a noisy neighbour.
Use containers when startup must be instant and repeated. A container starts in tens of milliseconds. A microVM starts in hundreds, and Firecracker has closed most of that gap. If you are genuinely starting one per HTTP request, containers still win. For a session, a build or an agent run, sub second boot is not the bottleneck.
Use a microVM when you are running code you did not write. This is the real line. AI agents executing generated shell commands, user submitted code in an online IDE, CI jobs from forked pull requests, plugin systems, multi tenant platforms where tenants push arbitrary workloads. In every one of those, a container escape is not a theoretical CVE, it is a customer reading another customer's data.
Use a microVM when a compliance boundary requires it. Some auditors treat shared kernel multi tenancy as unacceptable regardless of your configuration, and arguing with that is usually more expensive than complying.
Use a microVM when the workload needs its own kernel. Custom kernel modules, a specific kernel version, or anything touching kernel tunables that are global on a shared host.
The middle ground worth knowing about
It is not a binary. Two options sit between.
gVisor puts a user space kernel between your process and the host, intercepting syscalls and implementing most of them itself. It reduces the host kernel surface substantially without a hypervisor. The cost is compatibility, since not every syscall is implemented, and performance on syscall heavy workloads.
Kata Containers runs each pod inside a lightweight VM while presenting a normal container runtime interface. You keep your Kubernetes workflow and get VM isolation underneath. This is a good answer for teams who want the boundary without rebuilding their platform.
Both are legitimate and both belong on the list. Worth knowing the trade: gVisor gives you a smaller host kernel surface but not a separate kernel, and Kata gives you a real VM boundary with the operational weight of running it yourself. If you want the boundary without building the platform underneath it, that is what a Cube is.
How to decide
One question: could the code on this machine have been written by someone who wants to hurt me?
If no, containers are fine and you should not add complexity for a threat you do not have.
If yes, or if the answer is "not today but the roadmap says we will let users upload plugins next quarter", you need a kernel boundary, and you should design for it before that quarter arrives rather than after. Retrofitting isolation into a running multi tenant product is considerably harder than starting with it.
The mistake I see most is teams treating the container boundary as a security wall because it was described that way in a tutorial, and then discovering the distinction during an incident. Containers are excellent packaging with useful, real, but limited isolation. Knowing which parts are real is the whole job.
If you want to go a level deeper on the mechanics, I wrote about what actually happens inside a cgroup when your container is killed, and about why architecture mismatches produce such confusing errors.