Right now, on your machine, a process is being lied to.
It thinks it's alone on the computer. It thinks it owns the whole filesystem. It thinks its process ID is 1. None of that is true. It's running right next to everything else, on the same kernel — and the kernel is lying to its face.
That lie has a name. We call it a container.
Prefer to watch? Full 6-minute walkthrough with the animation:
There is no "container" in the kernel
Here's the first thing nobody tells you. Open the Linux kernel source, search for a thing called a "container." You'll find zero. There is no container object. The kernel has no idea what the word means.
So what are you actually running? A normal process. The same kind as your text editor. Just with a few settings flipped. Docker doesn't build a magic box around your app — it starts a plain process and flips those settings for you.
There are two settings that do all the work:
- The first controls what the process can see.
- The second controls what it can use.
That's the whole magic. See, and use.
Trick 1 — namespaces (what a process can see)
Normally every process on the machine shares one view of the world: the same list of process IDs, the same filesystem, the same network. A namespace hands one process its own private copy of that view.
- Put it in a PID namespace, and it sees itself as process
1— like the machine just booted for it. - Put it in a mount namespace, it gets its own filesystem root.
- Give it a network namespace, and it has its own network interface and its own IP.
So on the real host, your process might be number 842, sitting in a list of hundreds. Inside its namespace, it looks down and sees... 1. Same machine, same kernel. The process just can't see past the wall the kernel drew around it.
root inside, nobody outside
Here's the detail that trips everyone up: there's a user namespace too.
Inside the container, your process can be root — user zero, full power. Outside, on the host, that same process is a boring, unprivileged user.
Root on the inside. Nobody on the outside. Same process. That's namespaces doing their job — and it's the foundation of rootless containers.
Trick 2 — cgroups (what a process can use)
Namespaces decide what you see. The second trick decides what you get. It's called a cgroup — a control group.
A cgroup sets limits. This process gets two CPU cores, not all of them. Half a gig of memory, and not a byte more. Cross the memory limit? The kernel kills it on the spot (that's your OOM kill).
That's how one container can't starve every other one on the box. No limits, and one noisy app eats the whole machine.
The whole idea in one line
Namespaces are what you see. Cgroups are what you get.
A container is just a normal process wrapped in those two things. Nothing else is in the box — because there is no box.
Why it's not a virtual machine
This is the part people get wrong the most.
A virtual machine boots its own kernel, on fake hardware that a hypervisor pretends is real. A whole operating system, from scratch, every time.
A container has no kernel of its own. It borrows the host's kernel — the one that's already running.
That one difference explains everything:
- Speed. No kernel to boot means a container starts in milliseconds, where a VM takes seconds.
- Isolation. Sharing a kernel cuts both ways. One kernel bug can let a process break out of its container and onto the host. A VM doesn't share that wall.
So you're trading isolation for speed. That's the deal — and it's a fair one, as long as you know you're making it.
Build one by hand — no Docker
Don't believe there's no magic? Build a "container" yourself with two commands:
# a "container" with no Docker at all
unshare --pid --mount --net --uts --fork \
chroot ./rootfs \
/bin/sh
-
unshareflips on the namespaces. -
chrootswaps the root filesystem. - And you're dropped into a shell that thinks it's alone on a fresh machine.
No daemon, no image — just kernel features.
So what does docker run actually do?
Four steps, in pseudo-code:
unpackImage(image) // files become the new root
applyNamespaces(process) // its own PID, mounts, network
applyCgroups(process) // capped CPU and memory
exec(process) // now it's just... a process
Unpack the files. Wrap it in namespaces. Cap it with cgroups. Run it. That's the whole thing Docker does that felt like magic.
And the image everyone ships around? It's just a stack of files in a tarball, plus a little metadata saying which program to start. No operating system inside. No kernel inside. Just files.
One number to leave you with
The Linux manual page namespaces(7) lists eight separate kinds of namespace — eight different views the kernel can hand a single process: mount, PID, network, IPC, UTS, user, cgroup, and time. That's the whole toolbox a container is built from.
So next time someone draws a container as a little box floating above the machine — remember there's no box. There's your kernel, showing one process a smaller version of the truth. Namespaces for what it sees, cgroups for what it gets, one shared kernel underneath.
What did you think a container was, before this? Tell me in the comments.
I make Vlad's Stack https://www.youtube.com/channel/UCUO8Uo5LsEy1b9eRkrH6JNg — how the tools you use every day actually work, for people who write code. Full video walkthrough of this one is above.
Top comments (0)