How does Docker actually isolate a process — what's really different from a VM?
Containers get called "lightweight VMs," but they share the host kernel — real isolation comes from Linux namespaces and cgroups, not virtualized hardware.
LEARN BY DOINGPut your intuition to work3 steps
How does Docker actually isolate a process — what's really different from a VM?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#"Lightweight VM" is a useful lie
Docker containers get introduced as "lightweight virtual machines" so often that most engineers repeat the phrase without ever being asked to defend it in an interview. It's a useful first mental model and a dangerous final one, because the two technologies isolate a process using completely different mechanisms, with completely different security and performance implications. Understanding the difference is the fastest way to sound senior in a DevOps conversation.
#What a VM actually virtualizes
A virtual machine virtualizes hardware. A hypervisor (Type 1 like ESXi/KVM, or Type 2 like VirtualBox) presents each VM with virtual CPUs, virtual memory, and virtual devices. Each VM then boots its own, independent kernel on top of that virtual hardware — its own Linux or Windows kernel, its own device drivers, its own process table, completely unaware that the "hardware" underneath it is emulated. Two VMs on the same physical host are as isolated from each other as two separate physical machines, because neither one shares a kernel with the other.
#What a container actually isolates
A container virtualizes nothing at the hardware level. Every container on a host runs as an ordinary process, scheduled by the same Linux kernel as every other container and the host itself. What makes it look like an isolated machine is a combination of kernel features that change what that one process can see and use:
| Namespace | What it isolates |
|---|---|
| PID | The container's own process tree — its first process is PID 1 inside the namespace, even though it has a completely different PID on the host |
| NET | Its own network interfaces, IP addresses, routing table, and port space — this is why two containers can both bind to port 8080 without conflicting |
| MNT | Its own view of the filesystem mount table — this is what makes / inside the container look like a fresh Alpine or Ubuntu root instead of the host's filesystem |
| UTS | Its own hostname and domain name |
| IPC | Its own System V IPC objects and POSIX message queues, so unrelated containers can't signal each other through shared memory |
| USER | Maps container-side UIDs (including UID 0, "root") to unprivileged UIDs on the host, so "root in the container" doesn't have to mean root on the host |
Namespaces answer "what can this process see?" A process in a PID namespace genuinely cannot see, signal, or ps the processes running in a sibling container or on the host — the kernel simply filters them out of view. But it is still the same kernel, scheduling the same underlying threads on the same CPU cores.
#cgroups: the other half of the equation
Namespaces control visibility; cgroups (control groups) control consumption. A cgroup lets the kernel cap and account for how much CPU, memory, disk I/O, and even how many process IDs a group of processes can use. This is what backs docker run --memory=512m --cpus=1.5: the kernel's cgroup v2 controller enforces a hard memory ceiling (an out-of-memory kill scoped to that cgroup, not the whole host) and a CPU quota (a fraction of a core per scheduling period), and exposes live usage numbers that docker stats reads directly from /sys/fs/cgroup.
Put together: namespaces make a process think it's alone; cgroups make sure it can't hog the machine even though it isn't. Docker also relies on a union/overlay filesystem (OverlayFS) to stack read-only image layers under a thin writable layer per container — this is why pulling an image is fast (shared, cached layers) and why a container's disk writes vanish when it's removed unless you mount a volume.
#A real Dockerfile, and where each isolation piece shows up
# Stage 1: build the binary in a full Go toolchain imageFROM golang:1.22-alpine AS builderWORKDIR /srcCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 GOOS=linux go build -o /out/api ./cmd/api# Stage 2: ship only the binary in a minimal runtime imageFROM alpine:3.19RUN apk add --no-cache ca-certificates \&& addgroup -S app && adduser -S app -G appWORKDIR /appCOPY --from=builder /out/api /app/apiUSER appEXPOSE 8080ENTRYPOINT ["/app/api"]
The multi-stage build keeps the final image small by discarding the Go compiler entirely — the runtime image never sees go build, only the compiled binary. USER app matters more than it looks: without it, the process inside the container runs as UID 0. Thanks to the MNT and PID namespaces that UID 0 can't see the host filesystem or host processes, but if a container-escape bug is ever found in the runtime, "root inside" being mapped to "root on the host" (no USER namespace remapping) turns a contained bug into a host compromise. EXPOSE 8080 doesn't open a port by itself — it's documentation consumed at docker run -p; the actual port binding happens in the container's own NET namespace.
#VM vs. container, side by side
| Virtual Machine | Container | |
|---|---|---|
| Isolates | Hardware (via hypervisor) | A single kernel's view (via namespaces) + resource usage (via cgroups) |
| Kernel | Own kernel per VM | Shared host kernel |
| Boot time | Seconds to minutes (boots a full OS) | Milliseconds (starts a process) |
| Image size | Gigabytes (full OS) | Megabytes to low hundreds of MB |
| Density per host | Tens | Hundreds to thousands |
| Isolation strength | Strong — separate kernels, mature hardware-assisted boundary | Weaker — one kernel vulnerability can affect every container on the host |
🧠 Check your understanding
0/3 · 0/3 answered1. A container and the host it runs on both show process IDs starting from a small number for their own "first" process. Which kernel feature makes this possible?
2. You run a container with `--memory=256m` and it starts allocating far more than that. What actually happens?
3. Why is a container generally considered a weaker security boundary than a VM?