Docker & ContainersII · Linux InternalsNamespaces
Linux namespaces — the foundation of every container
What you'll learn
- Identify the eight namespace types Linux supports
- Inspect namespaces from the host with lsns and /proc
- Use unshare and nsenter to interact with namespaces directly
Prerequisites
None — start here.
Verified against Docker Engine 29.x · Docker Engine 28.x · Docker Compose 2.x · containerd 2.x · runc 1.2.x · BuildKit 0.20+ · Linux kernel 5.15+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-09
A namespace is a kernel feature that gives a process (or a group of processes) its own view of a global resource. Namespaces are how Linux makes a process believe it has its own machine, without actually giving it one.
Every container is, at its core, a process tree inside a set of namespaces. Understanding namespaces is the single most important under-the-hood skill in this course.
The eight namespace types
Linux 5.x supports the following namespaces. Each one wraps a different global resource:
| Namespace | Flag | What it isolates |
|---|---|---|
| Mount | CLONE_NEWNS | The filesystem mount table |
| PID | CLONE_NEWPID | Process IDs (the container’s PID 1 is PID N on the host) |
| Network | CLONE_NEWNET | Network interfaces, routing, iptables |
| UTS | CLONE_NEWUTS | Hostname and domain name |
| IPC | CLONE_NEWIPC | System V IPC, POSIX message queues |
| User | CLONE_NEWUSER | UIDs and GIDs (rootless containers) |
| Cgroup | CLONE_NEWCGROUP | Cgroup root (kernel 4.6+) |
| Time | CLONE_NEWTIME | Boot time (kernel 5.6+) |
Docker uses all of these by default except CLONE_NEWTIME (which is
rare in production).
Inspecting namespaces from the host
The Linux kernel exposes namespace info through /proc. Each
process has a /proc/<pid>/ns/ directory:
ls -l /proc/1/ns/readlink /proc/1/ns/pid /proc/1/ns/net /proc/1/ns/mnt /proc/1/ns/userThe dedicated tool lsns from util-linux gives you a clean view:
lsns -l$ docker run --rm --name nsdemo -d alpine sleep 3600
lsns -l | head -20
echo '---'
PID=$(docker inspect --format '{{.State.Pid}}' nsdemo)
echo "PID in host: $PID"
ls -l /proc/$PID/ns/NS TYPE NPROCS PID USER COMMAND
4026531835 cgroup 142 1 root /sbin/init
4026531837 user 142 1 root /sbin/init
4026531838 pid 142 1 root /sbin/init
4026531839 uts 140 1 root /sbin/init
4026531840 ipc 142 1 root /sbin/init
4026531841 net 142 1 root /sbin/init
4026531842 mnt 138 1 root /sbin/init
...
---
PID in host: 41278
lrwxrwxrwx 1 root root 0 Aug 8 12:34 cgroup -> cgroup:[4026532634]
lrwxrwxrwx 1 root root 1 Aug 8 12:34 ipc -> ipc:[4026532620]
lrwxrwxrwx 1 root root 0 Aug 8 12:34 mnt -> mnt:[4026532628]
lrwxrwxrwx 1 root root 0 Aug 8 12:34 net -> net:[4026532631]
lrwxrwxrwx 1 root root 0 Aug 8 12:34 pid -> pid:[4026532629]
lrwxrwxrwx 1 root root 0 Aug 8 12:34 user -> user:[4026532627]
lrwxrwxrwx 1 root root 0 Aug 8 12:34 uts -> uts:[4026532632]Notice that the container’s namespaces have different inode numbers than the host’s. The container’s PID namespace has only the container’s processes visible.
Using unshare directly
unshare lets you create namespaces without Docker. This is the
single most useful debugging tool in this course.
sudo unshare --pid --fork --mount-proc /bin/bash
echo "PID inside namespace: $$"
echo "PID from host: $(cat /proc/self/status | grep ^Pid:)"
exitsudo unshare --net /bin/bash
ip addr
echo '---'
ip route
exitunshare --user --map-root-user /bin/bash
id
cat /proc/self/uid_map
exitUsing nsenter to enter a container from the host
nsenter lets you attach to any process’s namespaces and run a
command there. Docker uses this internally for docker exec.
PID=$(docker inspect --format '{{.State.Pid}}' nsdemo)
sudo nsenter -t $PID -m -p -n -u -i -- /bin/sh
echo "PID inside: $$"
echo "PID from /proc: $(cat /proc/self/status | grep ^Pid:)"
ip addr show eth0
ls /
exitPID=$(docker inspect --format '{{.State.Pid}}' nsdemo)
echo "docker exec is essentially: nsenter -t $PID -m -p -n -u -i -- <cmd>""Each namespace in depth
Mount namespace
Each container has its own mount table. The container’s rootfs is
constructed from image layers via OverlayFS and mounted at /.
Anything not explicitly bind-mounted in is invisible to the
container.
PID namespace
The container’s init process is PID 1 inside the container’s PID namespace. On the host, the same process is some larger number (its host PID). The container can only see its own PIDs.
A consequence: from inside the container, ps shows only the
container’s processes. To see them all from the host, you use the
host’s ps.
Network namespace
The container has its own network stack: interfaces, routing table, iptables. By default, Docker attaches the container to a bridge; traffic to/from the container is NAT’d through the host. Other drivers (host, macvlan, ipvlan, none) work differently.
UTS namespace
Hostname isolation. hostname inside the container returns the
container’s hostname, not the host’s. By default, Docker uses the
container ID (or the first 12 characters) as the hostname.
IPC namespace
Isolation of System V IPC primitives and POSIX message queues. Almost never an issue, but it prevents cross-container IPC attacks within the same host.
User namespace
The basis for rootless Docker. Inside the container’s user namespace, UID 0 (root) maps to some unprivileged UID on the host. A process inside that namespace that tries to write to a host file denied to its host UID is rejected by the kernel.
Cgroup namespace
A virtualised view of the cgroup hierarchy. Each container sees a cgroup root that contains only its own cgroup subtree. Used by container runtimes to avoid leaking the host’s cgroup topology.
The namespace that does not exist
There is no namespace for “how much machine do I have”. /proc/cpuinfo,
/proc/meminfo, /proc/stat, /proc/loadavg and the
sched_getaffinity(2) call all report the host’s hardware, from
inside every container, no matter what limits you set. The mount
namespace gives the container its own /proc mount, but procfs
generates the same numbers into it.
This is not a bug and it is not going to change. cgroups and namespaces are separate kernel subsystems: a cgroup limit is enforced by the scheduler and the memory controller, and nothing about enforcing it rewrites what procfs reports. The consequence is that every runtime which auto-sizes itself from “the machine” sizes itself from the wrong machine.
CONTAINER=api
PID=$(docker inspect --format '{{.State.Pid}}' "$CONTAINER")
CG=$(awk -F: '{print $3}' "/proc/$PID/cgroup" | head -1)
echo "host CPUs: $(nproc)"
echo "container sees: $(docker exec "$CONTAINER" nproc)"
echo "affinity: $(taskset -pc "$PID" | sed 's/.*: //')"
echo "cpu.max: $(cat "/sys/fs/cgroup$CG/cpu.max")"
grep -E '^(nr_periods|nr_throttled|throttled_usec)' "/sys/fs/cgroup$CG/cpu.stat"host CPUs: 12
container sees: 12
affinity: 0-11
cpu.max: 200000 100000
nr_periods 240794
nr_throttled 0
throttled_usec 0That capture is a real container on a 12-CPU host limited to two
CPUs. Note the three lines that disagree: the container sees 12,
its affinity mask covers all 12, and its actual allowance is two.
nr_throttled 0 says this particular workload is not busy enough
for it to matter yet — which is what a healthy reading looks like,
and what makes a rising nr_throttled meaningful.
The two ways to close the gap:
- Tell the runtime the truth. Set
GOMAXPROCS=2,worker_processes 2,OMP_NUM_THREADS=2, or the equivalent, alongside--cpus 2. This keeps the quota model (burstable, shares idle capacity) and just stops the over-subscription. It is the right default, and it means the limit and the environment variable must be changed together — put them in the same Compose block so they cannot drift. - Use a cpuset instead of a quota.
--cpuset-cpus 0-1restricts which CPUs the container may run on, which does changesched_getaffinity(), sonprocreports 2 and every auto-sizing runtime gets it right with no configuration. The cost is rigidity: the container cannot use idle capacity elsewhere, and pinning several containers to the same cores creates contention the scheduler cannot route around.
Memory has the same shape with a different ending. free and
/proc/meminfo inside the container report host RAM, so anything
sizing a cache or a heap from “total memory” over-commits and gets
OOM-killed at a fraction of what it planned for. Modern JVMs are the
exception: UseContainerSupport has been on by default since JDK 10
and reads the cgroup limit rather than /proc/meminfo. Almost
nothing else does — check, per runtime, rather than assuming the JVM
fix generalises.
CONTAINER=api
PID=$(docker inspect --format '{{.State.Pid}}' "$CONTAINER")
CG=$(awk -F: '{print $3}' "/proc/$PID/cgroup" | head -1)
docker exec "$CONTAINER" head -1 /proc/meminfo
echo "cgroup memory.max: $(cat "/sys/fs/cgroup$CG/memory.max")"MemTotal: 32791060 kB
cgroup memory.max: 536870912Illustrative output
31 GiB visible, 512 MiB enforced. A cache that sizes itself at “25% of RAM” will ask for 8 GiB and die at 512 MiB, and the container’s own diagnostics will insist it had plenty of memory.
What breaks when namespaces break
- A container can see host PIDs → the PID namespace is shared or
leaking. Almost always a
--pid=hostaccident. - A container can see host network interfaces → the network namespace
is shared or
--network=host. - A container can read host files → a mount-namespace leak, typically via bind mount or rootfs misconfiguration.
- A container cannot see its own PIDs → the PID namespace was not
properly created. Re-check
unshare --pid.
Knowledge check
Knowledge check · 5 questions
Q1. Which of the following namespaces does Docker typically create by default? Select all that apply.
Q2. A container whose init process is PID 1 — what is its PID from the host's perspective?
Q3. Which tool can you use on the host to enter a container's namespaces and run commands inside it?
Q4. A container on a 12-CPU host is started with `--cpus 2`. What does `nproc` print inside it?
Q5. Running `free -m` inside a container reports the memory limit set with `--memory`.
Passing score: 75%. Answers are checked in this browser.
Where next
The next lesson covers cgroups — the resource-side companion to namespaces. After that, OverlayFS, capabilities, and the rest of the Linux internals the rest of the course assumes you understand.