Namespaces and cgroups: how isolation actually works
The two Linux kernel features that make containers possible, explained without hand-waving.
Containers are not a special kind of process
This is the single most important thing to internalize before going further: a container is just a regular process on the host machine. If you run a container and then look at the host's process list, you'll see it sitting right there alongside every other process — a normal PID, using the normal kernel, scheduled by the normal scheduler.
What makes it behave like an isolated little machine is two separate Linux kernel features working together:
- Namespaces — control what a process can see.
- cgroups (control groups) — control what a process can use.
Docker didn't invent either of these. They're kernel features that existed before Docker; Docker's real contribution was packaging them into an easy, standardized workflow (images, a CLI, a daemon) that ordinary developers could use without hand-rolling kernel syscalls.
Namespaces: controlling visibility
A namespace wraps a global system resource and gives a process (or group of processes) its own isolated view of it. The process thinks it's looking at the whole system, but it's actually only seeing a partitioned slice.
Linux has several kinds of namespaces, each isolating a different resource:
| Namespace | What it isolates |
|---|---|
| PID | Process IDs — a container sees its own process tree, starting from PID 1 |
| Network (net) | Network interfaces, IP addresses, ports, routing tables |
| Mount (mnt) | Filesystem mount points — the container sees its own root filesystem |
| UTS | Hostname and domain name |
| IPC | Inter-process communication (shared memory, semaphores) |
| User | User and group IDs — a process can be "root" inside the container but an unprivileged user outside |
Take the PID namespace as a concrete example. On the host, a container's main process might genuinely be PID 4821. But because it's launched inside its own PID namespace, from inside the container, that same process looks like PID 1 — as if it were the very first process the OS ever started, the way an init system normally would be. It cannot see PID 4820 or 4822 (other host processes, or other containers) at all. As far as that process is concerned, it's alone on the machine.
The network namespace works the same way: each container gets its own virtual network interface, its own IP address, its own routing table and its own view of "localhost". Two containers can both bind to port 8080 with zero conflict, because each is bound to port 8080 inside its own network namespace — they're not actually sharing a namespace the way two processes on bare Linux would be.
Put all of these together — PID, network, mount, UTS, IPC, user — and a process genuinely can't tell it's not running on its own dedicated machine. That illusion, built entirely from namespaces, is container isolation.
cgroups: controlling resource usage
Namespaces stop a process from seeing things it shouldn't. They do nothing to stop a process from consuming everything — a runaway container could still eat all the host's CPU or memory and starve every other container on the machine, exactly like the "noisy neighbor" problem from the previous lesson.
cgroups solve that. A cgroup lets the kernel put hard limits on how much CPU, memory, disk I/O, or network bandwidth a group of processes is allowed to consume. When you set a memory limit on a container, that value is really just a cgroup rule the kernel enforces: cross the line, and the kernel kills the process (an "OOM kill") rather than letting it degrade the whole host.
docker run --memory=512m --cpus=1 my-app
That single command line is quietly asking the kernel to create a cgroup capping this container at 512 MB of RAM and one CPU core's worth of scheduling time. If the app inside tries to allocate 600 MB, it gets killed — the host itself is never at risk.
Putting the two together
| Feature | Answers the question | Analogy |
|---|---|---|
| Namespaces | "What can this process see?" | Walls around a room |
| cgroups | "How much can this process use?" | A utility meter with a cap |
A container is the combination of both: a process (or small group of processes) running inside a set of namespaces that hide the rest of the system from it, wrapped in a cgroup that caps how much of the shared hardware it's allowed to consume.
Where the container's filesystem comes from
There's one more piece: the mount namespace gives a container its own view of the filesystem, but that filesystem has to come from somewhere. That "somewhere" is the image — a packaged, read-only filesystem snapshot containing the application and every file it needs. When a container starts, the engine mounts that image's filesystem as the container's root, using a union filesystem that layers a thin writable layer on top of the image's read-only layers, so the container can write files during its life without ever modifying the shared image underneath.
We'll go deep on images in the next module — for now, the key mental model is: namespaces + cgroups give you isolation, an image gives you a filesystem to be isolated with. Docker is the tooling that builds images and wires up namespaces/cgroups for you, so you never have to touch either directly.
Why this matters practically
You'll rarely — if ever — interact with namespaces or cgroups directly while using Docker day to day. So why learn this? Because almost every confusing container behavior traces back to one of these two mechanisms:
- "Why can't my container see files from another container?" → mount namespace.
- "Why did my container just get killed with no error message?" → cgroup memory limit (OOM kill).
- "Why does
ps auxinside my container only show one process?" → PID namespace. - "Why can two containers both use port 3000 without conflicting?" → network namespace.
Once you know isolation is "just" namespaces and cgroups, container behavior stops feeling like magic and starts feeling like a logical consequence of two kernel features doing exactly what they're designed to do.