AI

Docker Has No VM. Here's What's Actually Happening

Most developers think Docker is 'lightweight VMs.' It's not. It's a clever lie your kernel tells processes about what neighborhood they live in. Understanding the real mechanism changes how you debug, secure, and scale containers forever.

Docker Has No VM. Here's What's Actually Happening

Your container isn't isolated. Your kernel is just lying to it.

In 2019, a 200-person fintech startup I consulted with had a container 'escape' incident. Not a security breach. A misconfigured container was eating 100% CPU on a shared node and taking down neighboring services. Their engineers kept asking: 'how did one container affect another if they're isolated?' They'd been told containers were like mini-VMs. They weren't. Nobody had explained the actual mechanism, and that ignorance cost them a weekend outage affecting 300k users.

Here's the thing most Docker tutorials skip: there is no hypervisor. No virtualized hardware. No guest OS. When you run docker run ubuntu bash, you're running a regular Linux process. Same kernel. Same CPU. Same RAM chips. The 'isolation' is entirely a kernel bookkeeping trick.

Two syscalls run the whole show

Docker's isolation comes from two Linux kernel primitives: namespaces and cgroups. That's it. No magic.

Namespaces answer the question: what can this process see? Run lsns on any Linux box and you'll see them listed. There are seven namespace types that matter here:

mnt — filesystem mount points pid — process ID tree net — network interfaces, routing tables ipc — inter-process communication uts — hostname and domain name user — user and group IDs cgroup — cgroup root directory

When Docker creates a container, it calls clone() with flags like CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS. The child process gets a fresh copy of each namespace. Inside its PID namespace, that process thinks it's PID 1. It thinks it owns the whole network stack. It thinks its filesystem is the whole world. The kernel maintains a mapping table and translates every syscall back to the host reality. It's a coordinated hallucination.

But seeing is different from consuming. That's where cgroups come in.

cgroups are the bouncer, namespaces are the fake ID

Control groups (cgroups) answer: how much can this process use? CPU, memory, disk I/O, network bandwidth. They're organized as a hierarchy in /sys/fs/cgroup/. When you run docker run --memory=512m, Docker writes 536870912 to /sys/fs/cgroup/memory/docker/[container-id]/memory.limit_in_bytes. That's it. The kernel's memory subsystem reads that file and enforces it.

Back to that 2019 incident: the team had set memory limits but forgot CPU limits. Namespaces gave the rogue container its own view. cgroups should've capped its CPU shares. Without that --cpus flag, the kernel's completely fair scheduler (CFS) treats all processes equally across namespaces. One hungry process can starve neighbors. The container boundary meant nothing to the scheduler.

What this means for your actual work

The kernel is shared. Always. A kernel panic inside a container panics the host. A kernel exploit inside a container can escape to the host. This is why gVisor (Google, 2018) and Kata Containers exist: they add a real syscall interception layer or a micro-VM because sometimes you actually do need that hardware boundary.

For 95% of workloads, namespaces plus cgroups is enough and the performance overhead is near zero (unlike a VM which burns 10-15% on virtualization tax). But you have to set both. Namespaces without cgroup limits is a loaded gun. Check your docker inspect output right now. Look at HostConfig.CpuQuota. I'll wait.

Key insight: Docker isolation is a kernel feature, not a Docker feature. If you understand namespaces and cgroups, you understand Docker. Everything else is CLI sugar on top of clone() and /sys/fs/cgroup/.
OPEN IN REEDL_ FEED →← Back to feed