Systems

Linux Namespaces

The kernel isolation primitive behind every container

Linux namespaces are the kernel feature that gives a process its own private, virtualized view of a global system resource — process IDs, the network stack, mount points, the hostname, IPC objects, user IDs, and the cgroup root. First shipped in kernel 2.4.19 (2002) with mount namespaces and completed with user namespaces in 3.8 (2013) and time namespaces in 5.6 (2020), there are eight types, each independently created via clone(), unshare(), or setns(). Combine them with cgroups (which cap usage) and a container feels like its own machine while sharing a single kernel — this is exactly how Docker, Podman, and Kubernetes work.

  • Namespace types8 (mnt, uts, ipc, pid, net, user, cgroup, time)
  • First introducedMount ns — kernel 2.4.19 (2002)
  • Last major typeUser ns 3.8 (2013), time ns 5.6 (2020)
  • Syscallsclone(2), unshare(2), setns(2)
  • What it virtualizesThe view of a resource, not the usage
  • Pairs withcgroups (limits) + seccomp + capabilities

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

Why namespaces matter

Before namespaces, isolating a workload meant a full virtual machine: a second kernel, emulated hardware, gigabytes of RAM, and seconds of boot time. Namespaces offer a radically cheaper alternative. They let one kernel present many independent, mutually invisible views of itself to different groups of processes — so a container can boot in milliseconds, weigh megabytes, and still believe it is the only thing running on the machine.

The core idea is disarmingly simple. The kernel already tracks global resources: a single process table, one routing table, one hostname, one set of mount points. A namespace wraps one of those global tables in a partition so that processes inside see only their slice. Membership is per-process and inherited by children. Two processes in different network namespaces can both bind TCP port 443; two processes in different PID namespaces can both be "PID 1." Nothing is emulated — the kernel simply consults a different table depending on which namespace the calling process belongs to.

This is the primitive underneath Docker, containerd, Podman, LXC, systemd-nspawn, and the sandbox of every serverless platform. Understanding namespaces is the difference between treating containers as magic and being able to debug why a process inside one can't reach the network, or why "root in the container" is not root on the host.

The eight namespace types

Each namespace type virtualizes exactly one class of global resource. They are fully independent — a process can be in a new network namespace but share the host's PID and mount namespaces, or any other combination.

Namespaceclone flagVirtualizesSince
Mount (mnt)CLONE_NEWNSThe mount table — filesystem mount points and propagation2.4.19 (2002)
UTSCLONE_NEWUTSHostname and NIS domain name2.6.19 (2006)
IPCCLONE_NEWIPCSystem V IPC and POSIX message queues2.6.19 (2006)
PIDCLONE_NEWPIDProcess ID number space (own PID 1)2.6.24 (2008)
Network (net)CLONE_NEWNETInterfaces, routes, ports, iptables, ARP2.6.29 (2009)
UserCLONE_NEWUSERUID/GID mappings and per-namespace capabilities3.8 (2013)
CgroupCLONE_NEWCGROUPThe cgroup root directory (hides the host cgroup path)4.6 (2016)
TimeCLONE_NEWTIMECLOCK_MONOTONIC and CLOCK_BOOTTIME offsets5.6 (2020)

Note the acronym "UTS" comes from struct utsname, the structure returned by uname(2) — the UTS namespace is precisely what lets a container report its own hostname. The time namespace is the newest and deliberately narrow: it only offsets the monotonic and boottime clocks (so a checkpoint/restored container can preserve uptime), not the wall clock CLOCK_REALTIME.

How it works: the three syscalls

Every namespace is manipulated through three system calls, and knowing which does what is the whole mental model:

  • clone(fn, stack, flags, arg) — create a new child process and, for each CLONE_NEW* flag set, place that child into a brand-new namespace of that type. This is a single atomic step: the container's very first process is born already isolated. This is how a runtime spawns a container's init.
  • unshare(flags) — do not fork; instead move the calling process out of the namespaces it shares with its parent and into fresh ones. The unshare(1) command-line tool is a thin wrapper. A subtlety: unshare(CLONE_NEWPID) does not move the caller itself into the new PID namespace (its PID can't change) — the next child it forks becomes PID 1 of the new namespace.
  • setns(fd, nstype) — join an existing namespace, identified by an open file descriptor to /proc/[pid]/ns/<type>. This is how docker exec and nsenter drop a new shell into a container that is already running.

Namespaces are exposed in the filesystem as magic symlinks under /proc/[pid]/ns/. Each looks like net:[4026531992]; the number is the namespace's inode. Two processes are in the same network namespace exactly when those inode numbers match. A namespace stays alive as long as it has a member process or an open file descriptor / bind-mount referring to it — which is how you keep a network namespace around with no process in it (ip netns add bind-mounts it under /var/run/netns/).

Building a container by hand

The magic dissolves once you assemble a container from the primitives yourself. This shell sequence isolates hostname, PIDs, mounts, and the network — roughly what a container runtime does before execing your program:

# Enter fresh UTS, PID, mount, and network namespaces.
# --fork + --pid together make the shell's child PID 1 of the new PID ns.
# --mount-proc remounts /proc so `ps` reflects the new PID namespace.
sudo unshare --uts --pid --mount --net --fork --mount-proc bash

# Inside — set an isolated hostname (UTS namespace)
hostname container-01

# Only our own processes are visible; this bash is PID 1
ps -e            # -> just bash and ps

# The network namespace starts almost empty: loopback, and it's DOWN
ip link          # -> only "lo", state DOWN
ip route         # -> (nothing)

# Bring loopback up; we still have no route to the outside world yet
ip link set lo up

To actually reach the network, you create a veth pair — a virtual Ethernet cable with two ends. One end goes in the container's net namespace, the other stays on the host and plugs into a bridge that NATs traffic. This is precisely what Docker automates with the docker0 bridge, and what a Kubernetes CNI plugin generalizes to give every pod a routable IP.

The PID namespace and the init problem

The PID namespace deserves special attention because it changes process semantics. The first process in a new PID namespace is PID 1 inside it, and inherits init's two special responsibilities:

  1. Reaping orphans. When a process's parent dies, the orphan is reparented to PID 1, which must wait() on it or accumulate a zombie. A container PID 1 that never reaps leaks defunct entries in the process table.
  2. Signal semantics and teardown. Signals from other processes to PID 1 are ignored unless it has installed a handler — so a naive PID 1 that doesn't handle SIGTERM won't respond to docker stop and gets SIGKILLed after the grace period. And when PID 1 exits, the kernel SIGKILLs every remaining process in the namespace, collapsing it instantly.

This is the entire reason for lightweight init shims like tini (Docker's --init) or dumb-init: they sit at PID 1, forward signals to your app, and reap zombies. PID namespaces also nest — a PID 1 in an inner namespace has some other, larger PID as seen from the host, so the kernel maintains a mapping per level up to a nesting depth of 32.

User namespaces and rootless containers

The user namespace is the most security-relevant and the trickiest. It maps a contiguous range of UIDs/GIDs inside the namespace to a different range outside, via /proc/[pid]/uid_map and gid_map. A process can therefore be UID 0 — full root — inside its user namespace while being an unprivileged UID like 100000 on the host.

Crucially, capabilities are evaluated per user namespace. "Root in the container" holds every capability within that namespace (it can create other namespaces, mount filesystems it owns, etc.) but holds none of them against the host. This inverts the historic requirement that creating namespaces needs CAP_SYS_ADMIN: an unprivileged user can first create a user namespace (the one type that never required privilege) and, being root inside it, then create all the others. That single trick is the foundation of rootless Docker and Podman — containers with no daemon and no host root at all.

Namespaces vs cgroups — the two halves of a container

The single most common confusion is conflating namespaces with cgroups. They are orthogonal and answer different questions.

NamespacesCgroups
Question answeredWhat can this process see?How much can this process use?
MechanismPartition the view of a global resourceMeter and cap resource consumption
GovernsPIDs, net stack, mounts, hostname, IPC, UIDsCPU shares, memory, block I/O, PID count
Prevents "noisy neighbor"?No — a process can still hog all the CPU/RAMYes — hard limits and weights
Prevents seeing host processes?Yes (PID namespace)No
Syscall / interfaceclone/unshare/setns, /proc/[pid]/nscgroupfs at /sys/fs/cgroup (v2)
Kills a runaway process?NoYes (memory cgroup → OOM killer)

A useful slogan: namespaces are about isolation, cgroups are about allocation. A container that only used namespaces could still exhaust the host's memory and take down every other container; a container that only used cgroups would still see every host process and share its network. You need both. Docker's --memory, --cpus, and --pids-limit flags configure cgroups; its isolation of PIDs, network, and filesystem is namespaces.

Common misconceptions and pitfalls

  • "Namespaces are a security boundary like a VM." No. Every container shares one kernel; a single kernel bug is a potential escape. Namespaces isolate the view, not the attack surface. Real isolation layers on seccomp, dropped capabilities, MAC (AppArmor/SELinux), and often user namespaces — or a microVM (Firecracker, gVisor) for a separate kernel.
  • "A new network namespace has connectivity." It starts with only a down loopback and nothing else — no routes, no external interface. You must wire in a veth pair or macvlan to get traffic out.
  • "unshare(CLONE_NEWPID) puts me in the new PID namespace." It doesn't — your PID can't change mid-life. Only your next forked child becomes PID 1 of the new namespace. This is why unshare --pid needs --fork.
  • "Forgetting to remount /proc." A new PID namespace with the host's /proc still shows host processes to ps. You must mount a fresh proc inside the mount namespace (--mount-proc) for the PID view to be consistent.
  • "Root in the container is root on the host." Only if there is no user namespace. With user-namespace remapping, container root maps to an unprivileged host UID and holds no host capabilities.
  • "Zombies don't matter." Without a reaping PID 1, orphaned children pile up as defunct entries and can exhaust the PID limit. Use an init shim.

How to inspect namespaces

# List every namespace on the system with owning PIDs
lsns

# See which namespaces a given process belongs to (inode per type)
ls -l /proc/$$/ns/
# lrwxrwxrwx net -> 'net:[4026531992]'  mnt -> 'mnt:[4026531840]' ...

# Two processes share a namespace iff these inode numbers match
readlink /proc/1234/ns/net
readlink /proc/5678/ns/net

# Enter a running container's namespaces (what `docker exec` does)
PID=$(docker inspect -f '{{.State.Pid}}' my-container)
sudo nsenter --target "$PID" --net --pid --mount --uts ip addr

# Run a command in only a new mount + net namespace
sudo unshare --mount --net --fork bash

The inode-matching trick is the ground truth: namespace equality is inode equality on the /proc/[pid]/ns/* magic symlinks. Everything Docker shows you about isolation is ultimately explained by which of those inodes a process shares with the host and with its siblings.

Frequently asked questions

What is the difference between namespaces and cgroups?

They are orthogonal kernel features that containers combine. Namespaces control what a process can see — they partition a global resource so a process gets its own private view (its own PID 1, its own network interfaces, its own mount table). Cgroups control how much a process can use — they meter and cap CPU, memory, block I/O, and PIDs. A namespace can hide the host's other processes; only a cgroup can stop your process from eating all the RAM. Docker and Kubernetes use both together.

How many types of Linux namespaces are there?

As of modern kernels (5.6+) there are eight: mount (mnt, 2002, the first), UTS (hostname/domainname), IPC (System V IPC and POSIX message queues), PID (process IDs), network (net — interfaces, routes, firewall, ports), user (UIDs/GIDs and capabilities, the last major one, stabilized in 3.8), cgroup (virtualizes the cgroup root directory), and time (CLOCK_MONOTONIC/BOOTTIME offsets, added in 5.6). Each is created and joined independently.

What is the difference between clone, unshare, and setns?

clone() creates a new child process and, with CLONE_NEW* flags, places it into fresh namespaces in a single step — this is how a container's first process is born. unshare() moves the CALLING process into new namespaces without forking, dissociating it from the ones it shared with its parent. setns() moves the caller into an EXISTING namespace given a file descriptor referring to it (typically /proc/[pid]/ns/net) — this is how docker exec or nsenter join a running container. clone = new process + new namespaces; unshare = same process, leave; setns = same process, join.

Why does a PID namespace's first process have to be PID 1?

The first process created in a new PID namespace is assigned PID 1 inside it and becomes the namespace's init. It inherits init's special duties: it reaps orphaned children (zombies) whose parents died, and if it terminates, the kernel sends SIGKILL to every other process in the namespace, tearing it down. That is why a container needs a proper init (or the --init tini shim) — a naive PID 1 that ignores SIGTERM and never reaps zombies leaves defunct processes and blocks clean shutdown.

How do user namespaces enable rootless containers?

A user namespace maps a range of UIDs/GIDs inside the namespace to a different range outside it. A process can be UID 0 (root) inside its user namespace while being an unprivileged UID like 100000 on the host, using /proc/[pid]/uid_map and gid_map. Capabilities are then evaluated against the namespace, so 'root in the container' holds full capabilities inside but none over the host. This lets unprivileged users create the other namespaces (which normally require CAP_SYS_ADMIN) — the foundation of rootless Docker and Podman.

Do namespaces provide security isolation like a VM?

No — and this is the most dangerous misconception. Namespaces isolate the VIEW of resources, but every container shares one kernel. A single kernel vulnerability (a bad syscall, a driver bug) can be a container escape. Namespaces alone are not a security boundary; production isolation layers on cgroups, seccomp syscall filters, capability dropping, AppArmor/SELinux, and often user namespaces. For hard multi-tenant isolation people reach for microVMs (Firecracker, gVisor) that give each guest its own kernel.

What is a network namespace and how do containers talk to each other?

A network namespace is a private copy of the entire network stack: its own loopback, network interfaces, routing table, ARP cache, iptables/nftables rules, and port space — so two containers can both bind port 80 without conflict. A fresh net namespace starts with only a down loopback and no connectivity. Containers are wired up with a veth pair: one end lives in the container's namespace, the other in the host, plugged into a bridge (docker0) that routes and NATs traffic. Kubernetes replaces this with a CNI plugin that gives every pod a routable IP.