Linux Namespaces: The Isolation Primitive Behind Containers
What Linux namespaces actually do, the eight namespace types, how to inspect them on a live system, and why they make containers fundamentally different from virtual machines.
Published on • October 12, 2026
AI Assistant

“Containers are isolated processes” is Docker’s own one-line definition, and the isolation it refers to comes overwhelmingly from one kernel feature: namespaces. If you run Docker or Kubernetes for a living, namespaces are worth understanding properly — they explain why a container sees PID 1 as its own init, why port 8080 on the host and port 80 inside a container don’t collide, and why container isolation is fundamentally weaker than a VM’s.
What a namespace is
The namespaces(7) man page opens with a definition worth quoting nearly verbatim: a namespace “wraps a global system resource in an abstraction that makes it appear to the processes within the namespace that they have their own isolated instance of the global resource.” Changes inside the namespace are visible to other members of that namespace and invisible to everyone else. The man page then states the killer use case outright: “One use of namespaces is to implement containers.”
Note what this is: the kernel isn’t copying or virtualizing resources. It’s partitioning the views of them. Every process on the system still runs on the same kernel; it just sees a per-namespace version of PIDs, network stacks, mount trees, and so on.
The eight namespace types
Current Linux ships eight namespace types, each with a CLONE_NEW* flag used in the creation APIs (namespaces(7)):
| Namespace | Flag | Isolates |
|---|---|---|
| Cgroup | CLONE_NEWCGROUP | Cgroup root directory |
| IPC | CLONE_NEWIPC | System V IPC, POSIX message queues |
| Network | CLONE_NEWNET | Network devices, stacks, ports, etc. |
| Mount | CLONE_NEWNS | Mount points |
| PID | CLONE_NEWPID | Process IDs |
| Time | CLONE_NEWTIME | Boot and monotonic clocks |
| User | CLONE_NEWUSER | User and group IDs |
| UTS | CLONE_NEWUTS | Hostname and NIS domain name |
A concrete tour
PID namespaces isolate the process ID number space, so processes in different PID namespaces can have the same PID. PIDs in a new PID namespace start at 1: the first process created in the namespace is its “init” process (pid_namespaces(7)). If that init process terminates, the kernel kills every other process in the namespace with SIGKILL — which is why killing PID 1 in a container tears the whole container down. PID namespaces nest (maximum depth 32 since Linux 3.7), and a process is visible to its own namespace and each direct ancestor, but never the reverse: moving back up into an ancestor namespace is impossible.
Network namespaces give each container its own network stack. The Docker Engine security docs put it plainly: “Each container also gets its own network stack, meaning that a container doesn’t get privileged access to the sockets or interfaces of another container.” That stack includes network devices, IPv4/IPv6 stacks, routing tables, firewall rules, /proc/net, and the port space — so two containers can both bind port 80 without conflict, and traffic only flows between them where the host is configured to allow it.
Mount namespaces isolate mount points: a container gets its own mount tree, so mounting or unmounting a filesystem inside doesn’t touch the host. This pairs tightly with PID namespaces — as pid_namespaces(7) notes, after creating a new PID namespace, the child should mount a fresh procfs (mount -t proc proc /proc) so tools like ps show only the namespace’s own processes.
UTS namespaces isolate the hostname and NIS domain name — hostnamectl inside a container changes only the container’s view.
IPC namespaces isolate System V IPC and POSIX message queues, semaphore sets, and shared memory segments, so two containers can’t communicate through leftover shared memory.
Time namespaces isolate the boot and monotonic clocks (CLOCK_BOOTTIME and CLOCK_MONOTONIC), mainly useful for containers that must keep consistent time across migration or checkpoint/restore.
Cgroup namespaces virtualize the view of /proc/self/cgroup, so a process sees its own cgroup subtree as the root.
User namespaces isolate user and group IDs, and this is the security-relevant one. They let you be “root” inside a container while mapping that UID 0 to an unprivileged UID on the host. Docker supports this directly as the user namespace remapping feature (userns-remap, available since Docker 1.10, “available but not enabled by default”), which “allows for the root user in a container to be mapped to a non uid-0 user outside the container, which can help to mitigate the risks of container breakout.” Per user_namespaces(7), it’s impossible while in a user namespace to gain more privilege than the root user of that namespace has — and since Linux 3.8, creating a user namespace requires no privilege at all (with CONFIG_USER_NS enabled).
The API behind it all
Namespaces aren’t a Docker invention; they’re three system calls plus a directory in procfs (namespaces(7)):
clone(2)creates a new process; passingCLONE_NEW*flags creates new namespaces and makes the child a member of them.unshare(2)moves the calling process into new namespaces.setns(2)joins an existing namespace, identified by a file descriptor to one of the/proc/<pid>/ns/*files.
Creating namespaces with clone(2)/unshare(2) generally requires CAP_SYS_ADMIN; user namespaces are the exception. Namespaces are also real objects with lifetimes: one is torn down when its last member process exits — but an open file descriptor or a bind mount on /proc/<pid>/ns/<type> keeps it alive even with no members inside.
Inspecting namespaces on a live system
Every process exposes its namespaces in /proc/<pid>/ns/, with each entry a symlink carrying the namespace type and inode number:
$ ls -l /proc/$$/ns
lrwxrwxrwx. cgroup -> cgroup:[4026531835]
lrwxrwxrwx. ipc -> ipc:[4026531839]
lrwxrwxrwx. mnt -> mnt:[4026531840]
lrwxrwxrwx. net -> net:[4026531969]
lrwxrwxrwx. pid -> pid:[4026531836]
lrwxrwxrwx. user -> user:[4026531837]
lrwxrwxrwx. uts -> uts:[4026531838]
If two processes’ symlinks resolve to the same device and inode numbers, they’re in the same namespace — that’s the whole trick for comparing a container’s namespaces with the host’s:
$ readlink /proc/$$/ns/uts
uts:[4026531838]
From there, two tools from util-linux do the heavy lifting:
lsnslists all currently accessible namespaces, reading directly from/proc. Filter by process with-p PIDor by type with-t(supported types:mnt,net,ipc,user,pid,uts,cgroup,time).nsenterruns a program inside another process’s namespaces —nsenter -t <pid> -aenters all of them (-mmount,-uUTS,-iIPC,-nnetwork,-pPID,-Uuser, and so on), which is the standard way to debug a running container from the host.
How Docker wires them up
When you run docker run, the Docker security docs state that “behind the scenes Docker creates a set of namespaces and control groups for the container.” In practice: the container’s main process is created via clone(2) with the relevant CLONE_NEW* flags (so it becomes PID 1 in a fresh PID namespace), it gets its own mount namespace rooted at the image’s filesystem, its own network namespace (usually attached to a bridge), and its own UTS/IPC namespaces. Optional userns-remap adds the user-namespace mapping. Everything else you see — port publishing, volumes, hostname — is configuration on top of these primitives.
What namespaces do NOT isolate
Namespaces partition views; they don’t limit usage or privilege, and they don’t defend against kernel vulnerabilities:
- The kernel is shared. A container escape is, by definition, a kernel-level boundary crossing. See our containers vs. virtual machines deep dive for why that matters.
- Resources belong to cgroups, not namespaces — CPU, memory, and I/O accounting and limits are the separate control-groups mechanism, which “do[es] not play a role in preventing one container from accessing or affecting the data and processes of another container” but stops one container from starving the host.
- Privilege belongs to capabilities and LSMs. Docker drops all capabilities except an allowlist, so “root” inside a container has far less power than real root — and seccomp profiles plus AppArmor/SELinux policies (linked from the same security page) restrict syscalls and filesystem access. That defense-in-depth is exactly why root-in-container plus user-namespace mapping is still not the same boundary as a hypervisor.
Why this matters
The practical difference: a virtual machine virtualizes hardware through a hypervisor and runs its own kernel; a container partitions kernel resource views through namespaces and shares the host’s kernel. That’s why containers start in milliseconds and weigh megabytes, and also why the isolation boundary of a container is only as strong as the kernel — and the namespace code — underneath it. Namespaces are cheap and fast isolation of perspective, not isolation of trust. Design multi-tenant systems with that distinction firmly in mind.
Further reading
- namespaces(7) — overview, types,
/procfiles, and the namespace API (mirror with full description) - pid_namespaces(7) — PID 1 semantics, nesting, and process visibility
- Docker Engine security — how namespaces, cgroups, capabilities, and user-namespace remapping fit together
- lsns(8) and nsenter(1) — inspecting and entering namespaces