What Are Namespaces?
A Linux namespace wraps a global system resource in an abstraction that makes it appear to processes within the namespace that they have their own isolated instance of the resource. Changes to the global resource are visible to processes in the same namespace but invisible to processes in other namespaces.
Think of namespaces as one-way mirrors. A process inside a namespace looks out and sees only what the kernel wants it to see. The host (or a process in the parent namespace) can see everything — including all the processes inside all the namespaces. But from inside, the world looks small, private, and self-contained.
Namespaces do not provide resource limits — that is the job of cgroups (Part 3). Namespaces provide resource visibility control. A process in a PID namespace cannot see other processes, but it is not limited in how many processes it can create. A process in a network namespace cannot see the host's network interfaces, but it is not limited in how much bandwidth it can consume.
The distinction is critical: namespaces control what you can see; cgroups control what you can use. Together they create the complete container isolation model.
The Six Namespace Types
Linux provides six namespace types (seven if you count the newer cgroup namespace, but we focus on the original six that form the core of container isolation):
| Namespace | Flag | Isolates | Kernel Version |
|---|---|---|---|
| PID | CLONE_NEWPID |
Process IDs | 2.6.24 (2008) |
| Network | CLONE_NEWNET |
Network devices, ports, routing | 2.6.29 (2009) |
| Mount | CLONE_NEWNS |
Filesystem mount points | 2.4.19 (2002) |
| UTS | CLONE_NEWUTS |
Hostname and domain name | 2.6.19 (2006) |
| IPC | CLONE_NEWIPC |
System V IPC, POSIX message queues | 2.6.19 (2006) |
| User | CLONE_NEWUSER |
User and group IDs | 3.8 (2013) |
Every process on a Linux system belongs to exactly one instance of each namespace type. By default, all processes share the same set of namespaces (the "initial" or "root" namespaces). When you create a container, the runtime creates new namespaces and places the container's processes into them.
# View the namespaces of a process (PID 1 = init/systemd)
ls -la /proc/1/ns/
# Output:
# lrwxrwxrwx 1 root root 0 May 14 10:00 cgroup -> 'cgroup:[4026531835]'
# lrwxrwxrwx 1 root root 0 May 14 10:00 ipc -> 'ipc:[4026531839]'
# lrwxrwxrwx 1 root root 0 May 14 10:00 mnt -> 'mnt:[4026531840]'
# lrwxrwxrwx 1 root root 0 May 14 10:00 net -> 'net:[4026531992]'
# lrwxrwxrwx 1 root root 0 May 14 10:00 pid -> 'pid:[4026531836]'
# lrwxrwxrwx 1 root root 0 May 14 10:00 user -> 'user:[4026531837]'
# lrwxrwxrwx 1 root root 0 May 14 10:00 uts -> 'uts:[4026531838]'
# The numbers in brackets are inode numbers — unique identifiers for each namespace instance
# Two processes with the same inode share the same namespace
PID Namespace — Process Tree Isolation
The PID namespace is perhaps the most intuitive. It gives a process its own view of the process ID number space. The first process in a new PID namespace gets PID 1 — just like the init process on the host. It cannot see processes outside its namespace, and processes outside cannot be signalled from within.
This is why every Docker container has a PID 1 process. It is not a coincidence or a convention — it is a direct consequence of PID namespace isolation. The container's entrypoint process becomes PID 1 in its namespace, regardless of what PID it has on the host.
flowchart TD
subgraph HOST["Host PID Namespace"]
P1["PID 1: systemd"]
P2["PID 452: sshd"]
P3["PID 1089: containerd"]
P4["PID 2341: nginx (container)"]
P5["PID 2342: nginx worker"]
P6["PID 2343: nginx worker"]
end
subgraph CONTAINER["Container PID Namespace"]
C1["PID 1: nginx master"]
C2["PID 2: nginx worker"]
C3["PID 3: nginx worker"]
end
P4 -.->|"same process"| C1
P5 -.->|"same process"| C2
P6 -.->|"same process"| C3
style HOST fill:#132440,stroke:#3B9797,color:#fff
style CONTAINER fill:#16476A,stroke:#3B9797,color:#fff
style P1 fill:#3B9797,stroke:#132440,color:#fff
style P2 fill:#3B9797,stroke:#132440,color:#fff
style P3 fill:#3B9797,stroke:#132440,color:#fff
style P4 fill:#BF092F,stroke:#132440,color:#fff
style P5 fill:#BF092F,stroke:#132440,color:#fff
style P6 fill:#BF092F,stroke:#132440,color:#fff
style C1 fill:#BF092F,stroke:#132440,color:#fff
style C2 fill:#BF092F,stroke:#132440,color:#fff
style C3 fill:#BF092F,stroke:#132440,color:#fff
Notice the duality: the nginx master process is simultaneously PID 2341 on the host and PID 1 inside the container. Both are correct — they are just different views of the same process, mediated by the PID namespace boundary.
Hands-On: Creating a PID Namespace
You do not need Docker to experiment with namespaces. The unshare command lets you create new namespaces directly. Let us create a PID namespace and observe the isolation:
# Create a new PID namespace with its own /proc filesystem
# --pid: create new PID namespace
# --fork: fork before executing the command (required for PID namespaces)
# --mount-proc: mount a new /proc so 'ps' works correctly
sudo unshare --pid --fork --mount-proc bash
# Inside the new namespace, list all processes
ps aux
# Output:
# USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
# root 1 0.0 0.0 8312 5264 pts/0 S 10:00 0:00 bash
# root 2 0.0 0.0 10072 3456 pts/0 R+ 10:00 0:00 ps aux
# Only TWO processes visible! Bash is PID 1.
# The hundreds of host processes are completely invisible.
# Try to signal a host process (it will fail)
kill -0 452
# Output: bash: kill: (452) - No such process
# Exit the namespace
exit
The Zombie Reaping Problem
In traditional Linux, PID 1 (systemd/init) reaps zombie processes — children whose parents died before calling wait(). In a container, YOUR process is PID 1. If it spawns children and does not properly handle SIGCHLD, zombies accumulate. This is why tools like tini or dumb-init exist — they act as a proper init system inside containers, handling signal forwarding and zombie reaping so your application does not have to.
PID namespaces can also be nested. A container can create another PID namespace inside itself (this is how Docker-in-Docker works). Each level adds another layer of isolation, with the parent able to see the children but not vice versa.
Network Namespace — Virtual Network Stacks
A network namespace provides a process with its own complete network stack: network interfaces, IP addresses, routing tables, firewall rules, port numbers, and socket listings. A process in a network namespace cannot see or interact with network interfaces in other namespaces.
This is how containers get their own IP addresses and can all bind to port 80 without conflicts. Each container has its own network namespace with its own port space — port 80 in container A is completely separate from port 80 in container B.
The isolation is comprehensive. Each network namespace has:
| Resource | Per-Namespace Instance | Example |
|---|---|---|
| Network interfaces | Own set of NICs (virtual or physical) | eth0, lo |
| IP addresses | Independent address assignment | 172.17.0.2/16 |
| Routing table | Own routing decisions | default via 172.17.0.1 |
| Firewall rules | Independent iptables/nftables | Container-specific rules |
| Port bindings | Full 0–65535 port range | Multiple containers on :80 |
| Socket listings | /proc/net/* isolated |
Only own connections visible |
Hands-On: Creating Network Namespaces
The ip netns commands provide the most direct way to work with network namespaces. Let us create two namespaces and connect them with a virtual ethernet (veth) pair — this is exactly how Docker's bridge networking works:
# Create two network namespaces
sudo ip netns add container1
sudo ip netns add container2
# List all network namespaces
ip netns list
# Output:
# container2
# container1
# Run a command inside a namespace — check interfaces
sudo ip netns exec container1 ip addr
# Output: Only loopback (lo) exists, and it is DOWN
# 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN
# link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# Bring up loopback
sudo ip netns exec container1 ip link set lo up
# Create a virtual ethernet pair (like a virtual cable)
sudo ip link add veth0 type veth peer name veth1
# Move each end into a different namespace
sudo ip link set veth0 netns container1
sudo ip link set veth1 netns container2
# Assign IP addresses
sudo ip netns exec container1 ip addr add 10.0.0.1/24 dev veth0
sudo ip netns exec container2 ip addr add 10.0.0.2/24 dev veth1
# Bring up the interfaces
sudo ip netns exec container1 ip link set veth0 up
sudo ip netns exec container2 ip link set veth1 up
# Test connectivity — ping from container1 to container2
sudo ip netns exec container1 ping -c 3 10.0.0.2
# Output: 3 packets transmitted, 3 received, 0% packet loss
# Clean up
sudo ip netns delete container1
sudo ip netns delete container2
docker run, Docker creates a network namespace for the container, creates a veth pair, places one end in the container namespace (as eth0) and the other end on the docker0 bridge on the host. It then adds iptables rules for port mapping and NAT. The entire Docker bridge networking model is built on exactly the primitives shown above.
Port Mapping Under the Hood
When you run docker run -p 8080:80 nginx, Docker does not magically make port 80 appear on your host's port 8080. Instead, it creates an iptables DNAT rule: traffic arriving at host:8080 is rewritten to be destined for the container's IP on port 80, then routed through the veth pair into the container's network namespace. This is why iptables -t nat -L on a Docker host shows many dynamically-created rules — one per published port.
Mount Namespace — Isolated Filesystem Views
The mount namespace was the first namespace type added to Linux (kernel 2.4.19, 2002) — which is why its clone flag is simply CLONE_NEWNS ("new namespace") without a more specific name. It provides a process with its own view of the filesystem mount tree.
When a process is placed in a new mount namespace, it starts with a copy of the parent's mount tree. After that, any mount or unmount operations within the namespace are invisible to processes outside it, and vice versa. This is how containers get their own root filesystem — a fresh mount tree that points to the container image layers.
Mount namespaces are the mechanism behind several critical container features:
- Container root filesystem — The image's layers are mounted as the container's
/ - Volume mounts — Host directories bind-mounted into the container's mount tree
- tmpfs mounts — In-memory filesystems for secrets and temp data
- /proc and /sys isolation — Container-specific views of kernel interfaces
Hands-On: Isolated Mounts
# Create a new mount namespace
sudo unshare --mount bash
# Create a temporary directory and mount a tmpfs there
mkdir -p /tmp/secret-mount
mount -t tmpfs tmpfs /tmp/secret-mount
# Write something into it
echo "namespace-secret-data" > /tmp/secret-mount/secret.txt
cat /tmp/secret-mount/secret.txt
# Output: namespace-secret-data
# In ANOTHER terminal (host), check if the mount is visible:
mount | grep secret-mount
# Output: (nothing — the mount is invisible outside the namespace)
ls /tmp/secret-mount/
# Output: (empty — the directory exists but the tmpfs is not mounted here)
# Exit the namespace — the tmpfs and its data are destroyed
exit
This is powerful: you can mount filesystems, bind directories, and overlay layers inside a namespace, and none of it leaks to the host. When the namespace is destroyed, all its private mounts are automatically cleaned up.
--mount type=bind,source=/src,target=/dst,bind-propagation=shared for specific use cases like mounting storage that needs to be visible across namespace boundaries.
UTS Namespace — Hostname Isolation
UTS stands for "UNIX Time-sharing System" — a historical name from the utsname structure that holds the system's hostname and domain name. A UTS namespace gives a process its own hostname and NIS domain name, independent of the host.
This might seem trivial compared to PID or network isolation, but it matters for applications that use the hostname for identification, logging, or cluster membership. Without UTS isolation, every container would report the host's hostname, making log analysis and service discovery confusing.
# Check current hostname
hostname
# Output: my-laptop
# Create a new UTS namespace
sudo unshare --uts bash
# Change the hostname inside the namespace
hostname container-web-01
hostname
# Output: container-web-01
# In ANOTHER terminal (on the host), check:
hostname
# Output: my-laptop (unchanged!)
# The hostname change is completely isolated to the namespace
exit
When you run docker run --hostname myapp nginx, Docker creates a UTS namespace and sets the hostname within it. The container's /etc/hostname reflects this value, and tools like hostname, logging frameworks, and application configuration all see the isolated name.
Hostname in Kubernetes Pods
In Kubernetes, each Pod gets a UTS namespace with its hostname set to the Pod name (e.g., web-frontend-7d8f9b4c5-x2kn4). This is critical for StatefulSets where each Pod needs a stable, unique identity (e.g., mysql-0, mysql-1, mysql-2) for clustering and replication. The UTS namespace makes this possible without conflicting with the node's actual hostname.
IPC Namespace — Inter-Process Communication Isolation
The IPC namespace isolates System V IPC objects (shared memory segments, semaphores, message queues) and POSIX message queues. Without IPC namespace isolation, a process in one container could potentially read shared memory segments created by a process in another container — a serious security and correctness issue.
System V IPC uses numeric keys to identify shared resources. If two containers both create a shared memory segment with key 0x1234, they need to get independent segments — not accidentally share one. The IPC namespace ensures this.
The isolated IPC mechanisms include:
- Shared memory segments —
shmget(),shmat(),shmdt() - Semaphore sets —
semget(),semop() - Message queues —
msgget(),msgsnd(),msgrcv() - POSIX message queues —
mq_open(), mounted at/dev/mqueue
# List System V IPC resources on the host
ipcs
# Output shows shared memory segments, semaphores, message queues
# Create a new IPC namespace
sudo unshare --ipc bash
# Check IPC resources inside the namespace
ipcs
# Output: empty! No shared memory, no semaphores, no message queues.
# ------ Message Queues --------
# key msqid owner perms used-bytes messages
#
# ------ Shared Memory Segments --------
# key shmid owner perms bytes nattch status
#
# ------ Semaphore Arrays --------
# key semid owner perms nsems
# Create a shared memory segment inside the namespace
ipcmk -M 1024
# Output: Shared memory id: 0
# This segment is invisible to the host and other namespaces
exit
User Namespace — UID/GID Mapping
The user namespace is the most security-critical namespace type — and the most recently completed (kernel 3.8, 2013). It maps user and group IDs between the namespace and the host. A process can be root (UID 0) inside a user namespace while being an unprivileged user (e.g., UID 100000) on the host.
This is the key to rootless containers. Without user namespaces, a process running as root inside a container is also root on the host — if it escapes the container (via a kernel vulnerability, for example), it has full host privileges. With user namespaces, "root" inside the container maps to an unprivileged UID outside, limiting the blast radius of any escape.
flowchart LR
subgraph CONTAINER["Inside Container (User Namespace)"]
U0["UID 0 (root)"]
U1["UID 1 (daemon)"]
U33["UID 33 (www-data)"]
end
subgraph HOST["Host System"]
H100000["UID 100000 (unprivileged)"]
H100001["UID 100001 (unprivileged)"]
H100033["UID 100033 (unprivileged)"]
end
U0 -->|"maps to"| H100000
U1 -->|"maps to"| H100001
U33 -->|"maps to"| H100033
style CONTAINER fill:#16476A,stroke:#3B9797,color:#fff
style HOST fill:#132440,stroke:#3B9797,color:#fff
style U0 fill:#BF092F,stroke:#132440,color:#fff
style U1 fill:#3B9797,stroke:#132440,color:#fff
style U33 fill:#3B9797,stroke:#132440,color:#fff
style H100000 fill:#3B9797,stroke:#132440,color:#fff
style H100001 fill:#3B9797,stroke:#132440,color:#fff
style H100033 fill:#3B9797,stroke:#132440,color:#fff
UID/GID Mapping in Practice
The mapping is defined in /proc/[pid]/uid_map and /proc/[pid]/gid_map. Each line defines a range mapping:
# Format: [inside_start] [outside_start] [count]
# Map UID 0-65535 inside the namespace to UID 100000-165535 on the host
echo "0 100000 65536" > /proc/$$/uid_map
# With Docker, enable user namespace remapping in /etc/docker/daemon.json:
# {
# "userns-remap": "default"
# }
# Check the mapping of a running container
cat /proc/$(docker inspect --format '{{.State.Pid}}' mycontainer)/uid_map
# Output: 0 100000 65536
# Verify: files created as "root" inside container are owned by 100000 on host
docker run --rm -v /tmp/test:/mnt alpine touch /mnt/testfile
ls -la /tmp/test/testfile
# Without userns-remap: -rw-r--r-- 1 root root ...
# With userns-remap: -rw-r--r-- 1 100000 100000 ...
Rootless Docker in Action
Docker supports running the entire daemon without root privileges using user namespaces. Install rootless Docker with dockerd-rootless-setuptool.sh install, and the daemon runs as your regular user. All containers run with remapped UIDs. The trade-off: some features (like binding to privileged ports below 1024) require workarounds. But for development and CI/CD environments, rootless Docker dramatically reduces the attack surface.
Combining Namespaces — How Docker Uses All Six
A container is not just one namespace — it is the combination of all six namespace types working together. Each namespace handles one dimension of isolation, and together they create the complete container illusion:
flowchart TD
PROC["Container Process"] --> PID["PID Namespace
Own process tree"]
PROC --> NET["Network Namespace
Own IP, ports, routes"]
PROC --> MNT["Mount Namespace
Own filesystem"]
PROC --> UTS["UTS Namespace
Own hostname"]
PROC --> IPC["IPC Namespace
Own shared memory"]
PROC --> USR["User Namespace
Own UID mapping"]
PID --> ISO["Complete Isolation Illusion"]
NET --> ISO
MNT --> ISO
UTS --> ISO
IPC --> ISO
USR --> ISO
style PROC fill:#BF092F,stroke:#132440,color:#fff
style PID fill:#3B9797,stroke:#132440,color:#fff
style NET fill:#3B9797,stroke:#132440,color:#fff
style MNT fill:#3B9797,stroke:#132440,color:#fff
style UTS fill:#3B9797,stroke:#132440,color:#fff
style IPC fill:#3B9797,stroke:#132440,color:#fff
style USR fill:#3B9797,stroke:#132440,color:#fff
style ISO fill:#132440,stroke:#BF092F,color:#fff
When you execute docker run, the container runtime (runc) makes a series of system calls to create all the namespaces, configure them, and then exec the container's entrypoint process within them. The equivalent low-level operation looks like:
# What "docker run" does under the hood (simplified)
# 1. Create all namespaces with clone() or unshare()
sudo unshare --pid --net --mount --uts --ipc --user --fork bash
# 2. Set the hostname (UTS)
hostname my-container
# 3. Mount the container filesystem (Mount)
mount --bind /var/lib/docker/overlay2/merged /new-root
pivot_root /new-root /new-root/.old-root
umount /.old-root
# 4. Configure networking (Network)
# (container runtime creates veth pair and attaches to bridge)
# 5. Set up UID mapping (User)
echo "0 100000 65536" > /proc/$$/uid_map
# 6. Mount /proc for the PID namespace (PID)
mount -t proc proc /proc
# 7. Apply cgroup limits (not a namespace — covered in Part 3)
# 8. exec the entrypoint
exec nginx -g "daemon off;"
The runtime also selectively shares namespaces in some configurations. For example, in Kubernetes, containers within the same Pod share the network namespace (so they can communicate over localhost) and the IPC namespace (so they can use shared memory), but each has its own PID, mount, and UTS namespaces.
localhost and use shared memory for fast IPC — they are in the same network and IPC namespaces.
You can inspect which namespaces a Docker container uses with:
# Get the container's PID on the host
CONTAINER_PID=$(docker inspect --format '{{.State.Pid}}' my-container)
# List all namespace references
ls -la /proc/$CONTAINER_PID/ns/
# Output:
# lrwxrwxrwx 1 root root 0 ... cgroup -> 'cgroup:[4026532516]'
# lrwxrwxrwx 1 root root 0 ... ipc -> 'ipc:[4026532449]'
# lrwxrwxrwx 1 root root 0 ... mnt -> 'mnt:[4026532447]'
# lrwxrwxrwx 1 root root 0 ... net -> 'net:[4026532452]'
# lrwxrwxrwx 1 root root 0 ... pid -> 'pid:[4026532450]'
# lrwxrwxrwx 1 root root 0 ... user -> 'user:[4026531837]'
# lrwxrwxrwx 1 root root 0 ... uts -> 'uts:[4026532448]'
# Compare with host PID 1 — different inode numbers = different namespaces
ls -la /proc/1/ns/
# You can ENTER a container's namespaces from the host with nsenter
sudo nsenter --target $CONTAINER_PID --mount --uts --ipc --net --pid bash
# Now you are "inside" the container, seeing its filesystem, processes, network
Exercises
- PID Namespace Exploration — Use
unshare --pid --fork --mount-proc bashto create a PID namespace. Inside, start a background process withsleep 300 &. In another terminal, find that sleep process on the host withps aux | grep sleep. What PID does it have on the host? What PID does it have inside the namespace? - Network Namespace Communication — Create two network namespaces and connect them with a veth pair (as shown in this article). Then extend the exercise: create a third namespace and set up routing so all three can communicate. Hint: you will need a bridge or a namespace acting as a router.
- Docker Namespace Inspection — Run
docker run -d --name ns-test nginx:alpine. Usedocker inspectto get its PID, then compare/proc/1/ns/with/proc/[container-pid]/ns/. Which namespaces are different? Are any shared with the host? - Namespace Escaping (Thought Experiment) — If a container process somehow gained access to the host's
/proc/1/ns/netfile and calledsetns()on it, what would happen? Why does this demonstrate that namespace isolation depends on preventing access to/procon the host?
Conclusion & Next Steps
Namespaces are the first pillar of container isolation. They control visibility — what a process can see of the system around it:
- PID namespace — isolates the process tree; container sees only its own processes
- Network namespace — isolates network interfaces, IPs, ports, and routing
- Mount namespace — isolates the filesystem mount tree
- UTS namespace — isolates hostname and domain name
- IPC namespace — isolates shared memory, semaphores, and message queues
- User namespace — maps UIDs/GIDs for rootless security
But isolation is only half the story. A process that cannot see other processes can still consume all the CPU, exhaust all memory, or saturate all disk I/O — crashing the entire host. We need resource limits.
Next in the Series
In Part 3: Control Groups — Resource Management, we will explore cgroups — the kernel mechanism that puts hard limits on CPU, memory, I/O, and process count. While namespaces control what a process can see, cgroups control what it can use.