TL;DR — Linux namespaces are the kernel primitives that let one OS host run multiple isolated “worlds” — each with its own PIDs, mounts, network stack, and users. Modern container runtimes (Docker, containerd, runc, CRI-O) are mostly careful compositions of
clone(2)with the right flags, pluscgroups,seccomp, and capabilities layered on top.
Why Namespaces Matter
Every container you’ve ever started — whether through docker run, kubectl apply, or a bare runc invocation — is, at its core, a Linux process with a particular set of clone(2) flags applied at creation time. There is no “container” object in the kernel. There is no hypervisor. There is a process tree, scoped by namespaces, and bounded by cgroups.
This is the mental model the rest of this post depends on:
- Namespaces change what a process can see (other PIDs, network interfaces, mount points, hostname, UIDs).
- Cgroups change what a process can use (CPU, memory, I/O, PIDs).
- Capabilities, seccomp, AppArmor, SELinux change what a process can do with what it sees.
When people say “containers are just processes,” they mean specifically: namespaced, cgroup-bounded, capability-restricted processes. The rest — image layers, union filesystems, OCI image manifests, CRI plumbing — is packaging and orchestration around that core idea.
If you’ve ever debugged a stuck kubectl exec, a mount namespace leak, or a “why does my container see the host’s network?”, you’ve already brushed up against the machinery described here.
A Brief History: From chroot to the Eight Namespaces
The story starts in 1979 with chroot(2), which restricted a process to a subtree of the filesystem. chroot was process-scoped but had no kernel enforcement of escape, no concept of “isolation” beyond pathnames, and zero awareness of processes, networking, or users.
The modern namespace story begins with OpenVZ in the mid-2000s — a paravirtualization patch series that introduced many of the ideas later merged upstream. Between 2008 and 2016, the kernel absorbed namespaces one type at a time:
| Year | Namespace | Syscall flag | What it isolates |
|---|---|---|---|
| 2002 | Mount | CLONE_NEWNS | Filesystem mount points |
| 2008 | UTS | CLONE_NEWUTS | Hostname and NIS domain name |
| 2008 | IPC | CLONE_NEWIPC | System V IPC, POSIX message queues |
| 2008 | PID | CLONE_NEWPID | Process IDs |
| 2010 | Network | CLONE_NEWNET | Network devices, stacks, ports |
| 2012 | User | CLONE_NEWUSER | UIDs and GIDs |
| 2014 | Cgroup | CLONE_NEWCGROUP | cgroup root of the process |
| 2016 | Time | CLONE_NEWTIME | Boot and monotonic clocks |
That table is your reference for the rest of the post. Eight types, eight CLONE_NEW* flags, eight knobs a container runtime turns.
How the Kernel Represents a Namespace
A namespace is not a process. It is a refcounted kernel object that processes can hold a reference to via a nsproxy (a struct that bundles several namespace pointers together) and, in newer kernels, via idmapped mounts and the nsfs filesystem.
You can see this directly. Open a shell and:
ls -l /proc/self/ns/
On a modern kernel you’ll see something like:
cgroup ipc mnt net pid pid_for_children time time_for_children user uts
Each entry is a bind-mount-style reference to an nsfs inode. The interesting thing about nsfs is that the inode is the namespace — bind-mounting it is just acquiring a reference, and setns(2) operates on a file descriptor to that inode to attach a process to an existing namespace.
The /proc/[pid]/ns/ symlinks are how tools like nsenter, unshare, and runc re-enter namespaces without re-creating them. This is exactly how kubectl exec reaches into a running container’s mount and PID namespaces — it opens /proc/<pid>/ns/mnt and setns(2) into it.
The nsproxy Bundle
Internally, each task_struct (the kernel’s per-process struct) holds a pointer to a struct nsproxy that contains pointers to:
mnt_namespace(mount namespace)uts_namespaceipc_namespacepid_namespace(with a pointer topid_ns_for_childrenfor new children)net(network namespace)cgroup_namespaceuser_namespacetime_namespace
The time_namespace and cgroup_namespace were the last to land. The pattern is consistent: namespace creation is a per-namespace-type copy_*_ns() function, and the nsproxy is reference-counted so it can be shared between threads.
Aside —
CLONE_NEWTIMEis the only namespace whose creation is gated byCAP_SYS_ADMINin the user namespace that owns it, which makes it a fun edge case for unprivileged container tooling.
The Eight Namespaces in Practice
Let’s walk through each, with the syscall you use to enter it and a concrete way to observe it.
Mount (CLONE_NEWNS)
The oldest namespace and the trickiest. A mount namespace gives a process its own view of the filesystem mount table. pivot_root(2) lets the namespace “re-root” itself, which is what container start-ups do after chdiring into the image’s rootfs.
unshare --mount --fork bash
mount -t tmpfs none /tmp
echo "Mounted tmpfs inside the namespace"
Anything mounted in the host’s mount namespace is invisible here, and vice versa. This is the namespace that determines whether your container sees /etc/resolv.conf from the host or the bind-mount your runtime injected.
UTS (CLONE_NEWUTS)
CLONE_NEWUTS isolates the hostname and NIS domain name. Trivial but important: this is what makes hostname inside a container report the container’s ID, not the node’s.
unshare --uts --fork bash
hostname container-7
exec bash
IPC (CLONE_NEWIPC)
CLONE_NEWIPC separates System V IPC objects and POSIX message queues. Two processes in different IPC namespaces cannot pass each other messages via msgget(2) or share a shmget(2) segment. This matters for hardened multi-tenant hosts where an untrusted workload could otherwise attach to a shared memory segment left behind by a tenant who exited.
PID (CLONE_NEWPID)
The most misunderstood namespace. CLONE_NEWPID makes the new process PID 1 inside its own namespace. Critically, the host still sees the real, global PID. The mapping is maintained in the pid_namespace structure, and /proc/[pid]/status exposes it via the NSpid: field.
NSpid: 4711 1
Here, 4711 is the host PID and 1 is the PID as observed inside the container. The container’s PID 1 has special semantics: signals it does not handle are ignored, and its exit kills the namespace. This is why Docker’s init mode and Kubernetes’ shared PID namespace behavior are non-trivial — they decide who plays PID 1 and what that means for signal handling and zombie reaping.
Network (CLONE_NEWNET)
CLONE_NEWNET gives the process a private copy of the network stack: its own interfaces, routing tables, iptables rules, sockets, and /proc/net/. When you start a fresh network namespace, it has only a lo loopback (down by default). Container runtimes create a veth pair, put one end inside the container’s netns, and the other end on a bridge (typically cbr0, docker0, or cilium_host in CNI setups).
A practical way to see this:
unshare --net --fork bash
ip link # only lo, down
ip addr
Tools like Cilium and CNI plugins from Calico operate heavily on this namespace, often skipping bridges entirely with eBPF.
User (CLONE_NEWUSER)
CLONE_NEWUSER is the most counter-intuitive. It gives a process its own view of UIDs and GIDs, with the ability to map a range of unprivileged host UIDs to root-like container UIDs. The mapping is the critical part — it’s how rootless containers work.
The mapping is written via /proc/[pid]/uid_map and /proc/[pid]/gid_map:
# Inside the user namespace, container UID 0 (root) maps to host UID 1000
0 1000 1
When the process inside opens a file owned by UID 0, the kernel checks against the mapped UID on the host, which is 1000 — a normal user. This is what allows Podman in rootless mode and Docker’s rootless variant to run containers without actually being root on the host.
Aside — Unprivileged user namespaces are gated by
kernel.unprivileged_userns_clone=1in some distributions (notably Debian and some RHEL/Fedora builds). This is one of the most common reasons “Docker works on my laptop but not in this hardened image” tickets get filed.
Cgroup (CLONE_NEWCGROUP)
CLONE_NEWCGROUP is the newest in the family of “viewing” namespaces. It does not actually change which cgroups apply resource limits — that remains determined by the cgroup filesystem mount and the cgroup hierarchy. What it changes is the root the process sees when it walks /sys/fs/cgroup/.
Combined with cgroup v2 (unified hierarchy), this is what lets a container “see” its own cgroup as / while the host sees the real path. A practical guide to the unified hierarchy is in the kernel’s cgroup v2 documentation.
Time (CLONE_NEWTIME)
CLONE_NEWTIME lets a namespace offset CLOCK_BOOTTIME and CLOCK_MONOTONIC. Useful for testing time-dependent code without changing system time, and for replaying recorded workloads. It is the only namespace that requires the namespace to be owned (via the user namespace) by the calling process or a process with CAP_SYS_ADMIN in that user namespace.
Patterns in Production: How runc Composes Namespaces
If you read the runc source, you’ll find libcontainer/nsenter/ and a JSON spec (the OCI Runtime Spec) that describes the intended namespace layout. The relevant section of config.json looks like:
{
"namespaces": [
{"type": "pid"},
{"type": "network"},
{"type": "ipc"},
{"type": "uts"},
{"type": "mount"},
{"type": "cgroup"}
]
}
A user namespace entry is optional. When present, it usually looks like:
{
"type": "user",
"uidMappings": [{"containerID": 0, "hostID": 100000, "size": 65536}],
"gidMappings": [{"containerID": 0, "hostID": 100000, "size": 65536}]
}
The runtime:
- Creates the user namespace first (so the rest can run unprivileged if desired).
- Forks into a child that sets up cgroups (v1 or v2).
- Sets up the mount namespace and pivots into the rootfs.
- Applies seccomp and capability rules from the spec.
- Enters each remaining namespace via
setns(2)(using the parent’s file descriptors) or viaclone(2)-with-flag. execve(2)s the container entrypoint.
That ordering matters. The OCI Runtime Spec deliberately specifies namespace creation order, and runtime authors have a test suite (runtime-tools) that pins the order down. The runc RUNC_INIT process is worth reading: it is essentially a small C program whose entire job is namespace composition.
Kubernetes: The Namespace-as-a-Service View
At the Kubernetes layer, the relevant primitive is the Pod, and the interesting configuration is in the pod.Spec.Host* and securityContext fields. By default, a Pod gets all “standard” namespaces (pid, ipc, net, uts, mount). A namespace can be shared by setting shareProcessNamespace: true (PID) or by giving the container a hostNetwork: true (network).
For hardened workloads, the Kubernetes security context documentation describes the levers:
runAsNonRoot,runAsUser,runAsGroupinteract with the user namespace.allowPrivilegeEscalation: falseblockssetuidbinaries — a defense-in-depth layer on top of the namespace boundary.seccompProfileandapparmorProfileare not namespaces, but they are the next layer of the container’s “what can it do” surface.
Common Failure Modes and How to Spot Them
If you operate containers in production, you’ll eventually run into these:
1. “My container can’t see the host’s mount.”
The mount namespace is doing its job. If you need to debug, nsenter -t <pid> -m enters the container’s mount namespace and lets you inspect the actual table.
2. “My container’s hostname is the node’s hostname.”
Someone set HostNetwork: true and the UTS namespace was not created, or the container was started with --network=host. UTS is independent of network — passing --network=host should still isolate UTS, but check your runtime’s defaults.
3. “Zombies are leaking into PID 1 of the container.”
The container has no proper init, and PID 1 in the PID namespace is not reaping orphans. This is a namespace-aware version of a classic Unix bug; Docker’s --init flag or Kubernetes’ pod.spec.shareProcessNamespace with a proper init image is the fix.
4. “My rootless container can’t bind port 80.”
The host kernel will not let an unprivileged user namespace bind a port < 1024 unless the host has net.ipv4.ip_unprivileged_port_start set or the binary has CAP_NET_BIND_SERVICE in the init user namespace. This is by design and not a bug.
5. “Files I create in the container are owned by UID 100000 on the host.”
This is the user namespace UID mapping working. Either adjust the mapping in the OCI spec, or use a nobody UID inside the container that’s mapped to a more friendly host UID.
Where Namespaces End: The Limits of This Model
Namespaces give visibility isolation, not fault isolation. A kernel bug in mount(2) can be triggered by a process inside a mount namespace and crash the host. Namespaces do not give you defense against side channels, speculative execution bugs (like the early Spectre variants), or kernel-level vulnerabilities in the namespace code itself. For that, you want a real hypervisor (KVM, Firecracker, gVisor in a different mode).
The move toward gVisor, Kata Containers, and Amazon’s Firecracker reflects exactly this trade-off: when the trust level of a workload is high enough, you stop relying on the kernel namespace code and run an entirely separate kernel-mode user-space monitor or a full micro-VM.
Key Takeaways
- A container is a Linux process that was created with one or more
CLONE_NEW*flags and is bounded by cgroups and restricted by capabilities and seccomp. - There are eight namespace types:
mnt,uts,ipc,pid,net,user,cgroup, andtime. Each isolates a single class of kernel resources. - Namespaces are refcounted kernel objects reachable via
/proc/[pid]/ns/*, andsetns(2)is the primitive that lets tools likekubectl execandnsenter“enter” a running container. - The OCI Runtime Spec encodes the namespace composition as JSON, and runc is essentially a careful ordering of
clone,setns,pivot_root, andexecvecalls around that spec. - User namespaces are the foundation of rootless containers; understanding UID/GID mapping is required to debug any “rootless” or “permission denied at port 80” ticket.
- Namespaces give you visibility isolation only. For higher-assurance isolation, layer them with cgroups, seccomp, AppArmor/SELinux, and a hard VM boundary.
Further Reading
- namespaces(7) — Linux Programmer’s Manual
- The Linux Kernel’s cgroup v2 documentation
- OCI Runtime Specification
- How Docker works — by libcontainer maintainer
- Kubernetes Pod Security Standards
- LWN: Namespaces in operation — the canonical deep dive from 2012, still accurate in shape