Skip to main content
RunBook Academy

Docker & ContainersII · Linux InternalsNamespaces

Linux namespaces — the foundation of every container

Foundation⏱ ~30 min

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

Not yet marked complete on this device.

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:

NamespaceFlagWhat it isolates
MountCLONE_NEWNSThe filesystem mount table
PIDCLONE_NEWPIDProcess IDs (the container’s PID 1 is PID N on the host)
NetworkCLONE_NEWNETNetwork interfaces, routing, iptables
UTSCLONE_NEWUTSHostname and domain name
IPCCLONE_NEWIPCSystem V IPC, POSIX message queues
UserCLONE_NEWUSERUIDs and GIDs (rootless containers)
CgroupCLONE_NEWCGROUPCgroup root (kernel 4.6+)
TimeCLONE_NEWTIMEBoot 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:

Read-only / Safeinspect namespaces
ls -l /proc/1/ns/
Read-only / Saferesolve each namespace
readlink /proc/1/ns/pid /proc/1/ns/net /proc/1/ns/mnt /proc/1/ns/user

The dedicated tool lsns from util-linux gives you a clean view:

Read-only / Safelsns overview
lsns -l
Read-only / Safeinspect namespaces
$ 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.

Service impact possibleunshare basics
sudo unshare --pid --fork --mount-proc /bin/bash
echo "PID inside namespace: $$"
echo "PID from host: $(cat /proc/self/status | grep ^Pid:)"
exit
Service impact possibleunshare network
sudo unshare --net /bin/bash
ip addr
echo '---'
ip route
exit
Read-only / Safeunshare user
unshare --user --map-root-user /bin/bash
id
cat /proc/self/uid_map
exit

Using 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.

Service impact possiblensenter basics
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 /
exit
Read-only / Safedocker exec under the hood
PID=$(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.

Read-only / Safedoes the container agree with its own limit?
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 0

That 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:

  1. 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.
  2. Use a cpuset instead of a quota. --cpuset-cpus 0-1 restricts which CPUs the container may run on, which does change sched_getaffinity(), so nproc reports 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.

Read-only / Safewhat the container thinks its memory is, versus its limit
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: 536870912

Illustrative 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=host accident.
  • 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

  1. Q1. Which of the following namespaces does Docker typically create by default? Select all that apply.

  2. Q2. A container whose init process is PID 1 — what is its PID from the host's perspective?

  3. Q3. Which tool can you use on the host to enter a container's namespaces and run commands inside it?

  4. Q4. A container on a 12-CPU host is started with `--cpus 2`. What does `nproc` print inside it?

  5. 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.