Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Understanding Linux namespaces

By updated

Improve this page

A namespace is a Linux kernel mechanism that gives a process its own view of a system resource: the list of processes, the network interfaces, the mount points, the host name. The process keeps running on the same kernel, but it no longer sees the same thing as you. This page has you build an isolated environment by hand with unshare, check the isolation obtained, then take a Docker container apart to see that it is nothing more than an assembly of namespaces.

What you will learn

  • List a process's namespaces with lsns and /proc/<pid>/ns/.
  • Create an isolated shell with unshare and prove that the isolation is real.
  • Understand why /proc must be remounted in a PID namespace.
  • Get root rights in a user namespace, without sudo.
  • Enter a container's namespaces with nsenter to troubleshoot it.
  • Tell the cgroup namespace (a view) from the cgroup controllers (a limit).

Prerequisites

Every exercise on this page was run on Ubuntu 24.04 LTS (kernel 6.8) with util-linux 2.39.3, which provides unshare, lsns and nsenter, then replayed on 1 October 2026 with Docker 29.1 and Podman 4.9. sudo access is needed for most commands, and a container engine (Docker or Podman) for the last part. The commands work identically on Debian 13 and RHEL 10; only the namespace identifiers change from one machine to the next.

A level A lab is enough: a shell on a disposable machine. The exercises change nothing persistent, and each namespace created disappears when its last process ends.

The eight types of namespaces

The kernel does not offer a single isolation mechanism but eight independent namespaces, each partitioning a precise resource. You combine them à la carte: isolating the network without isolating processes is perfectly legitimate. It is this granularity that sets a namespace apart from a virtual machine.

NamespaceWhat it isolatesunshare option
PIDThe process table. The first process becomes PID 1.--pid
Mount (mnt)Mount points: each namespace has its own tree.--mount
Network (net)Interfaces, IP addresses, routing tables, firewall rules.--net
UTSThe host name and the NIS domain name.--uts
IPCInter-process communication: shared memory, message queues, semaphores.--ipc
UserThe mapping of UIDs and GIDs, hence the privileges.--user
CgroupThe view of the cgroup hierarchy, not the resource limits.--cgroup
TimeThe CLOCK_MONOTONIC and CLOCK_BOOTTIME clocks, hence the perceived uptime.--time

UTS stands for Unix Time-Sharing System, a historical legacy: this namespace does not touch time but the host name. The Time namespace, for its part, does not change the wall clock: date returns the same value inside and outside the namespace, only the uptime is shifted.

The diagram sums up this table around a process: each arrow is a partition you turn on with an unshare option. The dotted arrow recalls the misconception covered at the end of the page: the cgroup namespace limits nothing, it is the cgroup controllers that cap CPU and memory.

A process and its eight namespaces: PID for the process tree, Mount for mount points, Network for interfaces and routes, UTS for the host name, IPC for queues and semaphores, User for UIDs and GIDs, Cgroup for the view of the hierarchy and Time for the monotonic clocks, each with its unshare option. The Cgroup namespace points with a dotted arrow to the cgroup controllers, the only ones that limit CPU and memory.limits nothing, seeProcesspartitioned view of the kernelPID: process treeunshare --pidMount: mount pointsunshare --mountNetwork: interfaces, routesunshare --netUTS: host nameunshare --utsIPC: queues, semaphoresunshare --ipcUser: UIDs and GIDsunshare --userCgroup: view of the hierarchyunshare --cgroupTime: monotonic clocksunshare --timecgroup controllerslimit CPU and memory
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

See your system's namespaces

Each process exposes its namespaces in /proc/<pid>/ns/, a directory of symbolic links. The link target holds the namespace identifier, a unique inode number. Two processes that point to the same number share the same namespace: it is the only reliable criterion to know whether an isolation is real or imaginary.

Terminal window
ls -l /proc/self/ns/
lrwxrwxrwx 1 bob bob 0 juil. 12 18:22 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 bob bob 0 juil. 12 18:22 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 bob bob 0 juil. 12 18:22 mnt -> mnt:[4026531841]
lrwxrwxrwx 1 bob bob 0 juil. 12 18:22 net -> net:[4026531840]
lrwxrwxrwx 1 bob bob 0 juil. 12 18:22 pid -> pid:[4026531836]
lrwxrwxrwx 1 bob bob 0 juil. 12 18:22 pid_for_children -> pid:[4026531836]
lrwxrwxrwx 1 bob bob 0 juil. 12 18:22 time -> time:[4026531834]
lrwxrwxrwx 1 bob bob 0 juil. 12 18:22 time_for_children -> time:[4026531834]
lrwxrwxrwx 1 bob bob 0 juil. 12 18:22 user -> user:[4026531837]
lrwxrwxrwx 1 bob bob 0 juil. 12 18:22 uts -> uts:[4026531838]

Numbers close to 4026531835 belong to the initial namespaces, the ones created when the system boots. Anything isolated afterwards gets a clearly higher number. Remember this reflex: readlink /proc/self/ns/net before and after a command tells you in one second whether you really changed namespace.

The lsns command gives the overall view, one namespace per line, with the number of processes it contains and the oldest process. On a machine that runs containers, the count per type immediately reveals the partitioning activity.

Terminal window
lsns -n -o TYPE | sort | uniq -c | sort -rn
9 mnt
7 pid
7 cgroup
6 uts
6 net
6 ipc
2 user
1 time

A single time namespace and two user ones on this machine, against nine mnt: it is the classic signature of a Docker host. Container engines always isolate mounts, processes and the network, but very rarely time and users. This imbalance has security consequences, checked further down.

Create an isolated environment with unshare

unshare creates one or more namespaces then runs a command in them. It is the tool that lets you rebuild a container by hand, step by step, and understand what each option really brings.

  1. Create the PID, Mount and Network namespaces at once

    Each option requested adds a partition. The --fork option is mandatory with --pid: the process that calls unshare cannot change PID namespace itself, only its children are born in it. Without --fork, your shell would stay in the old namespace.

    Terminal window
    sudo unshare --pid --mount --net --fork bash

    Check straight away that the network is isolated. A brand-new network namespace only contains a loopback interface, lo, down, and nothing else:

    Terminal window
    ip -brief addr
    lo DOWN
    Terminal window
    ping -c1 -W1 1.1.1.1
    ping: connect: Network is unreachable

    No route, no network card, no connectivity. It is the starting point of every container: the engine then grafts a virtual veth interface onto it.

  2. Notice that ps still lies

    You are in a new PID namespace, and yet:

    Terminal window
    ps -p 1 -o pid,user,comm
    PID USER COMMAND
    1 root systemd
    Terminal window
    ps aux | wc -l
    696

    ps shows the host's systemd as PID 1 and 696 lines, that is 695 processes plus the header, exactly as if you had isolated nothing. The isolation is active all the same, and your shell knows it:

    Terminal window
    echo $$
    1

    The shell is PID 1 of its namespace. It is ps that gets it wrong, because it reads the /proc inherited from the host: --mount created a mount namespace, but it copied the existing mount table, /proc included.

  3. Remount /proc so that ps tells the truth

    The mount command with the proc type replaces the inherited /proc with a pseudo file system fed by the current PID namespace:

    Terminal window
    mount -t proc proc /proc
    ps aux
    USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
    root 1 0.0 0.0 7340 3620 ? S 18:22 0:00 bash
    root 7 0.0 0.0 10884 4560 ? R 18:22 0:00 ps aux

    Two processes: your shell and the command you have just run. The partition is now visible as much as it is real. This remount does not affect the host: the mount namespace confines it.

  4. Use the --mount-proc shortcut

    Once the mechanism is understood, unshare can do both operations in one go. The --mount-proc option implies --mount and remounts /proc automatically:

    Terminal window
    sudo unshare --pid --net --fork --mount-proc bash
    ps aux
    USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
    root 1 0.0 0.0 4328 3384 ? S 13:58 0:00 bash
    root 2 0.0 0.0 7896 4036 ? R 13:58 0:00 ps aux

    The shell is PID 1, ps is PID 2, and nothing else exists in this namespace. It is the form to remember for everyday use. Type exit to leave: the namespace's last process disappears, and the kernel destroys automatically the namespaces that became empty.

The user namespace, the key to rootless containers

The user namespace is the most useful one to understand, because it is what lets an ordinary user run containers. Its principle: set up a mapping table between the UIDs inside the namespace and the real UIDs on the host. You become root in the namespace while remaining an unprivileged user for the rest of the system.

The --map-root-user option asks for the simplest mapping: "UID 0 here is my UID over there". No sudo is needed.

Terminal window
# Outside the namespace
id
uid=1000(bob) gid=1000(bob) groups=1000(bob),27(sudo),990(docker)
Terminal window
unshare --user --map-root-user bash
id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)

You are root. The supplementary groups sudo and docker, which have no mapping in the namespace, show up under the name nogroup. The mapping table, exposed in /proc/self/uid_map, explains exactly what happened: three columns, the starting UID inside the namespace, the matching UID on the host, and the size of the range.

Terminal window
cat /proc/self/uid_map
0 1000 1

UID 0 of the namespace matches UID 1000 of the host, over a range of a single identifier. This root is therefore a façade root, and the kernel knows it. Any operation that touches a resource belonging to the host is refused:

Terminal window
touch /etc/preuve-userns
touch: cannot touch '/etc/preuve-userns': Permission denied

Root in the namespace, powerless outside. On the other hand, this root has the capabilities in its own namespace, which lets it create other namespaces without sudo:

Terminal window
unshare --net --fork ip -brief addr
lo DOWN

An unprivileged user has just created a network namespace. It is exactly the mechanism that rootless Podman, Buildah and unprivileged containers in general rely on.

The other namespaces in practice

The UTS, IPC and Time namespaces are simpler, but demonstrating them is instructive: each one is checked with a single command, and the effect stays strictly confined.

The UTS namespace gives an environment its own host name. It is what lets a container be called conteneur-web while the machine is called something else. The change is fully confined: the host never sees it.

Terminal window
hostname # master1
sudo unshare --uts --fork bash
hostname conteneur-web
hostname # conteneur-web
exit
hostname # master1, unchanged

Persistent network namespaces with ip netns

A namespace created by unshare dies with its last process. For a network lab, you often want the opposite: a namespace that survives between two commands. The ip netns subcommand meets that need by anchoring the namespace to a file in /run/netns/, which prevents it from being destroyed.

Terminal window
sudo ip netns add labns
ip netns list
sudo ip netns exec labns ip -brief addr
labns
lo DOWN

The namespace exists while no process runs in it, and each ip netns exec runs a command in it on demand. Once lo is up, loopback works inside, isolated from the host's:

Terminal window
sudo ip netns exec labns ip link set lo up
sudo ip netns exec labns ping -c1 127.0.0.1
1 packets transmitted, 1 received, 0% packet loss, time 0ms

A persistent namespace must be deleted explicitly, otherwise it stays in place until the next reboot. Get into the habit of cleaning up:

Terminal window
sudo ip netns delete labns
ip netns list

The list must come back empty. It is the tool of choice to simulate routers, firewalls or multi-site topologies on a single machine.

What a container really isolates

A container is not a kernel technology: it is an assembly of namespaces, cgroups and capabilities orchestrated by an engine. The best way to be convinced is to start a container, then inspect it with the tools you have just learnt. The image is nginx 1.29.8 on Alpine, pinned by its digest:

Terminal window
docker run -d --name mon-conteneur \
nginx:1.29.8-alpine@sha256:5616878291a2eed594aee8db4dade5878cf7edcb475e59193904b198d9b830de
PID=$(docker inspect -f '{{.State.Pid}}' mon-conteneur)
sudo lsns -p "$PID"
NS TYPE NPROCS PID USER COMMAND
4026531834 time 745 1 root /sbin/init
4026531837 user 738 1 root /sbin/init
4026533214 mnt 17 723602 root nginx: master process nginx -g daemon off;
4026533215 uts 17 723602 root nginx: master process nginx -g daemon off;
4026533216 ipc 17 723602 root nginx: master process nginx -g daemon off;
4026533218 pid 17 723602 root nginx: master process nginx -g daemon off;
4026533219 cgroup 17 723602 root nginx: master process nginx -g daemon off;
4026533220 net 17 723602 root nginx: master process nginx -g daemon off;

Read the first two lines carefully. The time and user namespaces show the host's PID 1 (/sbin/init) and hundreds of processes: the container shares them with the host. Only six namespaces out of eight are really isolated, which the 1 October 2026 run with Docker 29.1 reproduces identically. Comparing the identifiers leaves no room for doubt.

TypeHost (PID 1)nginx containerIsolated?
mntmnt:[4026531841]mnt:[4026533214]yes
utsuts:[4026531838]uts:[4026533215]yes
ipcipc:[4026531839]ipc:[4026533216]yes
pidpid:[4026531836]pid:[4026533218]yes
netnet:[4026531840]net:[4026533220]yes
cgroupcgroup:[4026531835]cgroup:[4026533219]yes
useruser:[4026531837]user:[4026531837]no, shared
timetime:[4026531834]time:[4026531834]no, shared

The lack of a user namespace is Docker's major security point in its default configuration: root in the container is root on the host. The ps command on the host confirms it: the nginx process runs there as root. If that process escapes its partition, it escapes with full powers. Docker can turn on a user namespace through the userns-remap option, but it is not the default behaviour.

Podman in rootless mode makes the opposite choice and perfectly illustrates the previous section. The same nginx, started without sudo, believes it is root:

Terminal window
podman run --rm docker.io/library/nginx:1.29.8-alpine@sha256:5616878291a2eed594aee8db4dade5878cf7edcb475e59193904b198d9b830de id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

But seen from the host, this process belongs to the ordinary user who started it, not to root, which limits the damage of an escape:

PID USER COMMAND
727603 bob nginx

The mapping table comes from the delegated ranges in /etc/subuid, which podman unshare shows directly. The first line maps your account to root, the second delegates 65,536 extra UIDs for the image's other users:

Terminal window
podman unshare cat /proc/self/uid_map
0 1000 1
1 100000 65536

Enter a container with nsenter

nsenter does the opposite of unshare: instead of creating namespaces, it joins those of an existing process. It is the troubleshooting tool par excellence, because it does not depend on the container engine and works even when the Docker daemon no longer answers.

Terminal window
sudo nsenter --target "$PID" --mount --uts --ipc --pid --net --cgroup -- sh

You get the equivalent of a docker exec, with the container's host name, file system and process table:

# hostname
2e17ffa2b5a9
# ps -ef
PID USER TIME COMMAND
1 root 0:00 nginx: master process nginx -g daemon off;
30 nginx 0:00 nginx: worker process

The really valuable trick is to join only the network namespace. You then keep the host's file system, hence your tools, while observing the container's network. It is the answer to the classic case of the minimal image that contains neither ip, nor ss, nor curl, or only their BusyBox version:

Terminal window
sudo nsenter --target "$PID" --net -- ip -brief addr
sudo nsenter --target "$PID" --net -- ss -tln
lo UNKNOWN 127.0.0.1/8 ::1/128
eth0@if356 UP 172.17.0.3/16
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 511 [::]:80 [::]:*

You see the container's IP address and port 80 listening, without installing anything in the image. Remember this reflex: to diagnose a container's network, nsenter --net beats docker exec in almost every case.

The cgroup namespace limits nothing

It is the most widespread misconception about this lsns output. Seeing a cgroup namespace on a container does not mean its memory or CPU are limited. The cgroup namespace virtualises the view of the cgroup hierarchy: it makes the process believe its cgroup is the root. Nothing more.

On the host, nginx belongs to a cgroup with a well-identified path, a scope named after the container ID:

Terminal window
sudo cat /proc/$PID/cgroup
0::/system.slice/docker-2e17ffa2b5a9a90bc24fac62ecce9da06ebc481b11ecf57b3d563643d4bbdf0d.scope

Seen from inside the container, the same cgroup shows up like this:

Terminal window
docker exec mon-conteneur cat /proc/self/cgroup
0::/

Same cgroup, two different views. The namespace hid the real path, which avoids disclosing the host's tree and lets the container's tools reason about a clean root. The proof that nothing is limited takes one command: this container does have a cgroup namespace, and yet its memory is unlimited.

Terminal window
docker exec mon-conteneur cat /sys/fs/cgroup/memory.max
max

The limitation comes from the cgroup controllers, which the engine only configures if you ask for them. The same container started with quotas gives a very different result:

Terminal window
docker run -d --name conteneur-limite --memory=64m --cpus=0.5 \
nginx:1.29.8-alpine@sha256:5616878291a2eed594aee8db4dade5878cf7edcb475e59193904b198d9b830de
docker exec conteneur-limite cat /sys/fs/cgroup/memory.max
docker exec conteneur-limite cat /sys/fs/cgroup/cpu.max
67108864
50000 100000

Both containers have their own cgroup namespace, but only the second is constrained: 64 MiB of memory, and 50,000 microseconds of CPU per 100,000-microsecond period, that is half a core. Sharing resources is the topic of cgroups; namespaces only partition visibility.

Remember the formula: namespaces decide what a process sees, cgroups decide what it consumes, and capabilities decide what it is allowed to do. A container is all three at once.

Clean up your lab

Namespaces created by unshare disappear on their own when you leave the shell, but containers and persistent namespaces remain. Always finish with this clean-up, and check that the list of persistent network namespaces is empty:

Terminal window
docker rm -f mon-conteneur conteneur-limite
sudo ip netns delete labns 2>/dev/null
ip netns list

Troubleshooting

The errors met during these exercises are always the same, and they all have a simple cause. This table lists the ones actually produced while writing this page, with the fix that works, checked command by command.

SymptomCauseSolution
ps aux shows every host process in a PID namespace/proc is the host's, inherited by the mount namespaceRun mount -t proc proc /proc in the namespace, or start unshare with --mount-proc
The "isolated" shell sees every host network interfaceThe --net option was forgotten: no network namespace was createdAdd --net and check with readlink /proc/self/ns/net
unshare: write failed /proc/self/uid_map: Operation not permittedUbuntu 23.10+ restricts unprivileged user namespaces through AppArmorCheck sysctl kernel.apparmor_restrict_unprivileged_userns and set it to 0 for a lab
bash: fork: Cannot allocate memory after unshare --pid without --forkThe calling process did not enter the new PID namespaceAlways pair --fork with --pid
ip -brief addr fails in an nsenter --mountYou are using the image's ip, often a BusyBox one without the -brief optionJoin only --net to keep the host's tools
nsenter: cannot open /proc/<pid>/ns/pid: No such file or directoryThe target process is dead, or you are not rootReread the PID with docker inspect -f '{{.State.Pid}}' and prefix with sudo

Key points

  • A namespace partitions the view of a resource, not its consumption. Eight types exist: PID, Mount, Network, UTS, IPC, User, Cgroup, Time.
  • Isolation is only obtained for the namespaces explicitly requested. Without --net, the host's network stays fully reachable.
  • --fork is mandatory with --pid: only the process's children are born in the new PID namespace.
  • As long as /proc is not remounted, ps shows the host's processes. The isolation is real; it is the display that lies.
  • The user namespace maps internal UID 0 onto your real UID: root inside, without any privilege outside. It is the foundation of rootless containers.
  • A default Docker container shares the host's user and time namespaces: root in the container stays root on the machine.
  • The cgroup namespace virtualises the view of the hierarchy; it limits no resource. Limits come from the cgroup controllers.
  • nsenter --target <pid> --net is the network diagnostic tool to prefer: it gives the container's network with the host's tools.

Knowledge check

Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

The origins of namespaces

The mechanism was not born with containers. The first namespace, for mount points, appeared as early as 2002 in kernel 2.4.19, inspired by the Plan 9 system from Bell Labs. The UTS and IPC namespaces followed in 2006, PID and Network in 2008, then User in 2013, which finally unlocked unprivileged containers. Cgroup arrived in 2016 and Time in 2020 with kernel 5.6.

The arrival of Docker (in French) in 2013 therefore invented nothing at kernel level. Its contribution was to assemble existing building blocks and an image format into a usable tool, where earlier technologies required considerable system expertise. Today, Kubernetes (in French) orchestrates thousands of containers that all rely, ultimately, on the clone, unshare and setns system calls you have just used by hand.

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn