Linux Namespaces and cgroups: The Kernel Primitives That Make Containers Possible
When you run docker run nginx, something remarkable happens in about 300 milliseconds: a new isolated execution environment appears, with its own process tree, its own network stack, its own filesystem view, and its own resource limits — all without a hypervisor, without booting a kernel, without hardware virtualization.
This is made possible by two Linux kernel features: namespaces and cgroups. Container runtimes like Docker, containerd, and podman are, at their core, orchestrators that configure these two kernel primitives and then execute your process inside them.
Understanding namespaces and cgroups doesn’t just satisfy intellectual curiosity. It explains container behavior that otherwise seems magical or arbitrary, makes debugging easier, informs better container security decisions, and gives you the vocabulary to understand what Kubernetes is actually managing.
Namespaces: Isolation via Virtualized Views
A namespace wraps a global system resource and makes a process believe it has its own private instance of that resource. Processes in different namespaces can’t see or affect each other’s namespaced resources.
Linux currently has eight namespace types:
| Namespace | Flag | Isolates |
|---|---|---|
| Mount | CLONE_NEWNS |
Filesystem mount points |
| UTS | CLONE_NEWUTS |
Hostname and NIS domain name |
| IPC | CLONE_NEWIPC |
System V IPC, POSIX message queues |
| PID | CLONE_NEWPID |
Process IDs |
| Network | CLONE_NEWNET |
Network devices, IP addresses, routing tables, ports |
| User | CLONE_NEWUSER |
User and group IDs |
| Cgroup | CLONE_NEWCGROUP |
cgroup root directory |
| Time | CLONE_NEWTIME |
Boot and monotonic clocks (Linux 5.6+) |
Each Docker container gets its own instance of most of these. Let’s walk through them.
PID Namespace
In a PID namespace, the first process gets PID 1. It cannot see processes in the parent namespace. From the host, you can see the container’s processes with their real PIDs.
|
|
This has an important implication: PID 1 in a container is special. In Linux, PID 1 is responsible for reaping zombie processes (adopting orphans and waiting on them). If your container’s PID 1 doesn’t handle SIGTERM and reap children, you’ll get zombie processes and containers that don’t stop cleanly.
This is why minimal base images like scratch can cause issues when your app spawns subprocesses — there’s no init process to handle reaping. Solutions:
|
|
Experimenting with PID Namespaces
|
|
Network Namespace
A network namespace gives a process its own complete network stack: interfaces, IP addresses, routing tables, iptables rules, and socket table. Processes in different network namespaces cannot communicate directly — they need a virtual ethernet pair (veth pair) bridged between namespaces.
This is exactly what Docker does for every container:
Host network namespace Container network namespace
───────────────────── ──────────────────────────
eth0 (192.168.1.100) eth0 (172.17.0.2)
docker0 bridge (172.17.0.1) ←──veth pair──→ eth0@if5
iptables NAT rules lo (127.0.0.1)
|
|
This manual process is essentially what container runtimes automate.
Mount Namespace
A mount namespace gives a process its own view of the filesystem. Changes to mount points in one namespace don’t affect others. This is how containers get their own root filesystem.
|
|
Docker uses this to:
- Create an overlay filesystem combining the image layers as the container root
- Bind-mount volume paths from the host into the container
- Create
/proc,/sys,/devmounts specific to the container
Overlay Filesystems
Container images are made of layers. The overlay filesystem merges them:
Image layer (read-only): /usr, /lib, /etc (from ubuntu:24.04)
Image layer (read-only): /app (from your COPY instruction)
Container layer (writable): any writes go here
↓
Merged view (what the container sees): everything above unified
|
|
UTS Namespace
The simplest namespace — isolates hostname and domain name:
|
|
User Namespace
User namespaces are the most powerful and the most nuanced. They map user and group IDs between namespaces: UID 0 (root) inside the container can map to an unprivileged UID on the host.
|
|
This is the core of rootless containers — container root maps to an unprivileged host user, so a container escape doesn’t grant host root.
|
|
IPC Namespace
Isolates System V IPC (shared memory, semaphores, message queues) and POSIX message queues. Containers in different IPC namespaces can’t communicate via these mechanisms.
|
|
nsenter: Entering Namespaces
nsenter is the tool for entering a running process’s namespaces from the host — invaluable for debugging:
|
|
cgroups: Resource Control
While namespaces control what a process can see, control groups (cgroups) control what it can use — CPU, memory, I/O, network bandwidth, and more.
Linux has two versions:
- cgroups v1: Original implementation, controllers are mounted separately, complex hierarchy
- cgroups v2: Unified hierarchy, all controllers under
/sys/fs/cgroup, the modern standard (default in recent kernels and distros)
|
|
The cgroup Hierarchy
cgroups are organized as a tree. A child group inherits limits from its parent and can set stricter limits but not looser ones.
/sys/fs/cgroup/
├── system.slice/ ← systemd unit slice
│ ├── docker.service/
│ └── sshd.service/
├── user.slice/
│ └── user-1000.slice/
└── docker/ ← Docker's container cgroups
├── <container-id-1>/
└── <container-id-2>/
|
|
Memory Control
|
|
When a container exceeds its memory limit, the kernel’s OOM killer sends SIGKILL to a process in the cgroup. Kubernetes exposes this as OOMKilled in kubectl describe pod.
CPU Control
cgroups v2 uses a CPU weight system and CPU bandwidth control:
|
|
High throttled_usec means the container is CPU-throttled — it wants more CPU than its limit allows. This manifests as increased latency even when the host has spare CPU capacity.
|
|
I/O Control
|
|
Creating cgroups Manually
You don’t need Docker to use cgroups:
|
|
How Docker Puts It Together
When you run docker run --memory=256m --cpus=1 -v /data:/data nginx, here’s what actually happens:
1. Pull/verify image layers
2. Create overlay filesystem
→ Mount namespace: new overlay mount from image layers
→ Writable layer added on top
3. Create network namespace
→ New network namespace
→ Create veth pair: one end in host (connected to docker0 bridge), one in container
→ Assign IP from Docker's subnet (172.17.x.x)
→ Configure iptables NAT rules on host for port publishing
4. Create PID namespace
→ New PID namespace
→ Container process will be PID 1 within it
5. Create UTS namespace
→ Set container hostname (from --hostname or container ID)
6. Create IPC namespace
→ Isolated IPC objects
7. Set up cgroups
→ Create /sys/fs/cgroup/docker/<container-id>/
→ Write memory.max = 268435456 (256MB)
→ Write cpu.max = "100000 100000" (1 CPU)
8. Clone the process into all namespaces
→ syscall: clone(flags=CLONE_NEWPID|CLONE_NEWNET|CLONE_NEWNS|CLONE_NEWUTS|CLONE_NEWIPC)
9. Add process to cgroup
→ Write PID to /sys/fs/cgroup/docker/<container-id>/cgroup.procs
10. exec() the entrypoint
→ execve("/docker-entrypoint.sh", ...)
All of this is orchestrated by the container runtime (containerd, runc) based on an OCI runtime spec. The runtime spec is a JSON document describing exactly which namespaces, cgroup limits, mounts, and process to create.
|
|
Seccomp and Capabilities: The Other Security Layers
Namespaces and cgroups are isolation and resource control. Two more kernel features complete the container security picture:
Capabilities break root’s privileges into ~40 granular permissions. A container process might run as root (UID 0) inside the namespace but have only the capabilities it actually needs:
|
|
Seccomp filters system calls using Berkeley Packet Filter (BPF) rules. Docker’s default seccomp profile blocks ~44 syscalls that containers rarely need but that are historically exploitable:
|
|
Practical Implications for Container Operations
Why “OOMKilled” happens: The kernel’s OOM killer fires when a process in the cgroup tries to allocate memory beyond memory.max. Set limits thoughtfully — too tight and you’ll see OOMKills under load; too loose and a misbehaving container can starve other workloads.
Why CPU throttling hurts latency: Even when the host has spare CPU, a container at its cpu.max limit gets throttled. P99 latency spikes while CPU usage looks healthy. The fix is raising the CPU limit, not adding more containers.
Why container networking has overhead: Each container packet traverses virtual ethernet, the Docker bridge, iptables rules, and back. For high-throughput services, host networking (--net=host) removes this overhead at the cost of namespace isolation.
Why rootless containers matter: User namespace mapping means a container escape doesn’t grant host root. All modern container runtimes (Podman by default, Docker with rootless mode) support this.
Why pid=host is dangerous: --pid=host removes PID namespace isolation — the container can see and send signals to all host processes. Rarely needed, often requested, generally a bad idea.
Why privileged containers are effectively uncontained: --privileged disables seccomp filtering, grants all capabilities, and allows access to all host devices. It’s essentially running a process on the host with a different filesystem. Use it only when you explicitly need host-level access (like building container images or running a container runtime inside a container).
Tools for Exploration
|
|
Understanding namespaces and cgroups transforms container operations from “I run docker commands and things happen” to genuine comprehension of the isolation model. The next time a container misbehaves, OOMKills, throttles, or leaks into the host, you’ll know exactly where in the kernel to look.
Comments