Linux Namespaces: The Kernel Primitive Behind Every Container
How each of the Linux kernel's namespace types isolates a specific global resource, and why containers are just processes with a curated set of them.
Docker, Podman, and every other Linux container runtime rely on the same underlying kernel feature: namespaces. There’s no special “container” object in the Linux kernel — a container is simply a regular process that’s been placed into a specific combination of namespaces, each of which makes some global system resource appear to be its own private instance.
The core idea: virtualizing a global resource
Without namespaces, resources like the process ID table, hostname, or network interfaces are genuinely global — every process on the system sees the same list of PIDs, the same hostname, the same network interfaces. A namespace wraps one such resource so that processes inside the namespace see their own private, isolated view of it, independent of what the rest of the system sees.
# See which namespaces a running process belongs to
ls -la /proc/$$/ns/
PID namespaces
A PID namespace gives its member processes their own process ID numbering, starting from 1. The process that creates a new PID namespace becomes PID 1 inside that namespace, even though it has an entirely different PID in the parent (host) namespace’s view.
unshare --fork --pid --mount-proc /bin/bash
ps aux # inside the new namespace, this shell is PID 1
Being PID 1 comes with its own responsibility: PID 1 in any namespace is expected to reap zombie child processes, which is why minimal container images often need a tiny init process (tini, dumb-init) rather than running the application directly as PID 1.
Mount namespaces
A mount namespace gives its members their own view of the filesystem mount table — mounting or unmounting something inside the namespace doesn’t affect the host’s view, and vice versa (subject to mount propagation settings). This is the primitive underlying a container’s isolated root filesystem.
unshare --mount /bin/bash
mount -t tmpfs tmpfs /mnt # invisible outside this namespace
Network namespaces
A network namespace provides an entirely separate network stack — its own interfaces (other than a fresh lo), routing table, firewall rules, and port space. Two processes in different network namespaces can both bind port 80 without conflict, since neither can see the other’s sockets at all.
ip netns add myns
ip netns exec myns ip addr show
ip link add veth0 type veth peer name veth1
ip link set veth1 netns myns
A veth pair — a virtual Ethernet cable with one end in each namespace — is the standard way to connect an isolated network namespace back to the host or to a bridge, which is exactly how container runtimes give containers network connectivity while keeping their network stack isolated.
UTS, IPC, and user namespaces
The UTS namespace isolates hostname and NIS domain name, letting a container report its own hostname independent of the host’s. The IPC namespace isolates System V IPC objects and POSIX message queues, so unrelated containers can’t signal each other through shared memory segments or semaphores. The user namespace maps UIDs/GIDs inside the namespace to a different (often unprivileged) range outside it — a process can be “root” (UID 0) inside its user namespace while mapping to an entirely unprivileged UID on the host, which meaningfully limits the blast radius of a container escape.
unshare --user --map-root-user /bin/bash
id # uid=0(root) inside the namespace
cat /proc/self/uid_map
cgroup namespaces
A cgroup namespace, added more recently than the others, virtualizes a process’s view of its own cgroup membership path, so tools running inside a container see cgroup paths relative to the container rather than the host’s full absolute path — mostly a hygiene and information-hiding improvement rather than a security boundary on its own.
Composing namespaces: what a “container” is
A container runtime like runc (the low-level engine underneath Docker and most others) creates a new process placed into a fresh instance of most or all of these namespace types simultaneously, combined with cgroups for resource limiting and typically a restricted set of capabilities and a seccomp filter for defense in depth. unshare demonstrates the individual primitives directly:
unshare --fork --pid --mount --uts --ipc --net --mount-proc /bin/bash
hostname container1
Running that single command creates something that, from the inside, looks remarkably like a container — because that’s precisely what a container is: ordinary Linux processes, wrapped in a deliberately chosen combination of namespaces.
Inspecting namespaces on a running system
lsns lists every namespace currently in use on the system, along with which processes belong to each — useful for understanding exactly what isolation boundaries a given container runtime actually set up, rather than taking its documentation’s word for it:
lsns -t net
nsenter --target <pid> --net --pid /bin/bash
nsenter does the inverse of unshare — instead of creating new namespaces, it joins an existing process’s namespaces, which is exactly how docker exec and similar “shell into a running container” commands are implemented under the hood.