Namespaces & cgroups: What a Container Really Is
Here is the most clarifying sentence in this entire Part, and the one the overview
promised: there is no such thing as a container in the Linux kernel. Run ps on the host and you
will not find a “container” — you’ll find ordinary processes. Docker, containerd, and the rest are
tools that assemble a few existing kernel features into something we call a container. Once you see
the pieces, the magic disappears, and that’s the goal. You can’t operate, debug, or secure something
you think is magic.
A container is a normal Linux process — the same process model from Part 1 — with two things bolted on:
- Namespaces, which control what the process can see. (Isolation.)
- cgroups, which control how much the process can use. (Resource limits.)
That’s the whole definition. Everything else is packaging.
Namespaces: a private view of the system
Section titled “Namespaces: a private view of the system”A namespace is a kernel mechanism that gives a process its own private copy of some global resource. Normally every process shares one list of process IDs, one network stack, one set of mount points. A namespace says: this group of processes gets its own. Same kernel, same machine — but a partitioned view, as if each container had the place to itself.
The kernel offers several namespace types. The ones that make a container feel like a container:
| Namespace | What it isolates | What the container sees |
|---|---|---|
| PID | Process IDs | Its own process tree; its main process is PID 1 |
| Mount (mnt) | Filesystem mount points | Its own root filesystem (the image), not the host’s |
| Network (net) | Network stack | Its own interfaces, IPs, ports, routing table |
| UTS | Hostname & domain | Its own hostname |
| IPC | Shared memory, semaphores | Its own inter-process comms |
| User | User & group IDs | Can map “root inside” to an unprivileged user outside |
The PID namespace makes the point vividly. Inside the container, your app is PID 1 and sees only a handful of processes. On the host, that same running program has some large PID and sits in the host’s full process tree. One process, two views.
INSIDE the container (PID namespace) ON the host (real tree) PID COMMAND PID COMMAND 1 node server.js ◄── "I'm PID 1" 1 /sbin/init (systemd) 7 npm … 48213 node server.js ◄── same process! 48230 npmcgroups: how much the process can use
Section titled “cgroups: how much the process can use”Namespaces hide the rest of the machine, but a hidden machine is still shared. A process that can’t see its neighbours can still starve them — eat all the CPU, exhaust memory, hammer the disk. That’s the “noisy neighbour” problem, and namespaces do nothing about it.
Control groups (cgroups) are the second half. A cgroup is a kernel feature that meters and caps the
resources a group of processes may consume: CPU shares, a hard memory ceiling, block-I/O bandwidth, the
number of processes. Put a container’s processes in a cgroup with memory.max = 512M and the kernel
enforces it — exceed the limit and the kernel’s OOM killer steps in, container-locally, without taking
down the host or the neighbours.
NAMESPACES = what you can SEE CGROUPS = how much you can USE ┌──────────────────────┐ ┌──────────────────────────────┐ │ container's own: │ │ this group is capped at: │ │ • process tree │ │ • 0.5 CPU │ │ • root filesystem │ │ • 512 MiB RAM (hard ceiling) │ │ • network stack │ │ • 100 MiB/s disk I/O │ │ • hostname │ │ • 1024 processes │ └──────────────────────┘ └──────────────────────────────┘ isolation resource limitsYou can watch a real container’s enforced limits — they’re just files the kernel exposes:
# Set a memory limit at run timedocker run --memory=512m --cpus=0.5 myapp
# The runtime translates that into a cgroup the kernel enforces —# read the limit back as a file (cgroup v2):docker exec <container> cat /sys/fs/cgroup/memory.max # → 536870912Why this demystification matters
Section titled “Why this demystification matters”Seeing through the abstraction changes how you work, and it ties straight back to the book’s thread: what manual, error-prone step does this remove, and how does it make production safer?
Before namespaces and cgroups, isolating apps on one box meant either trusting them not to interfere (they did) or giving each one a whole VM (slow and wasteful). Both forced a manual, judgment-heavy trade-off — how much do I over-provision to be safe? cgroups turn that guess into a declared, kernel-enforced contract: this process gets at most this much, full stop. A memory leak in one container can no longer take down everything else on the host. That blast-radius containment is a production safety property, and it comes free from the kernel, not from anyone’s vigilance.
And because a container is “just a process,” your Part 1 skills all still work. You can strace it,
read its /proc entry, check its memory in logs and troubleshooting —
from the host you’re debugging an ordinary process that happens to have a private view. Nothing exotic.
This is also why a container can’t run a different OS kernel — there is only the host kernel doing the namespacing. It’s the mechanical reason behind the trade-off the previous page described. With the what settled, the next pages cover the packaging: how that filesystem view is built and shipped as an image.
The architect’s lens
Section titled “The architect’s lens”Namespaces and cgroups are the two kernel features a runtime chooses to assemble into a “container” — see them as adoptable mechanisms, not magic:
- Why do they exist? Because the old ways to share one box were both bad: trust apps not to interfere (they did) or give each a whole VM (slow, wasteful). Namespaces give a process its own view (its own PID tree, root filesystem, network stack), and cgroups give it a budget — together, VM-like isolation without a second kernel.
- What problem do they solve? Namespaces solve visibility isolation (one app can’t see or touch
another’s processes, mounts, or network); cgroups solve the noisy-neighbour problem namespaces leave
open, turning “how much do I over-provision to be safe?” into a kernel-enforced contract like
memory.max = 512M. - What are the trade-offs? It’s all one shared kernel — there’s no “container” object, so the
isolation is only as strong as the kernel’s namespacing (weaker than a VM’s hardware boundary), and a
container can’t run a different OS kernel than the host. The upside is that a container is “just a
process,” so
strace,/proc, and your Part 1 tooling all still work. - When should I avoid relying on them alone? For untrusted multi-tenant or strict-compliance workloads where namespace isolation isn’t a strong enough boundary — wrap those in a VM (the trade-off the previous page named).
- What breaks if I remove them? Remove namespaces and every container shares the host’s PID tree, mounts, and network — no isolation. Remove cgroups and one memory leak can OOM the whole host and every neighbour on it; the blast-radius containment that comes free from the kernel disappears.
Check your understanding
Section titled “Check your understanding”- Run
pson a container host and you won’t find a “container.” What will you find, and what does that tell you about what a container really is? - What is the difference in purpose between a namespace and a cgroup? Give the one-word summary of each.
- Explain the PID-namespace example: how can one running program be PID 1 in one view and PID 48213 in another?
- Namespaces isolate but don’t prevent the “noisy neighbour” problem. Why not, and what fixes it?
- Using the book’s thread, explain how cgroups turn a manual over-provisioning guess into a kernel-enforced safety property.
Show answers
- You find ordinary processes — there is no “container” object in the kernel. A container is just a normal Linux process with namespaces and cgroups bolted on; “container” is a convention the tooling assembles, not a kernel primitive.
- A namespace controls what a process can see (isolation); a cgroup controls how much it can use (limits). One word each: isolation and limits.
- The PID namespace gives the process group its own private process tree, so inside it the app is PID 1 and sees only a few processes. The host has the real, global tree, where that same running program has a large PID like 48213. One process, two views.
- Namespaces only hide the rest of the machine; a hidden machine is still shared, so a process can still starve neighbours by eating all the CPU or memory (the “noisy neighbour” problem). cgroups fix it by metering and capping resource use.
- Before cgroups, isolating apps meant either trusting them not to interfere or giving each a whole VM,
forcing a manual how much do I over-provision to be safe? guess. cgroups replace that guess with a
declared, kernel-enforced contract (e.g.
memory.max = 512M), so a leak in one container can’t take down the host — blast-radius containment that comes from the kernel, not human vigilance.