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
lsnsand/proc/<pid>/ns/. - Create an isolated shell with
unshareand prove that the isolation is real. - Understand why
/procmust be remounted in a PID namespace. - Get root rights in a user namespace, without
sudo. - Enter a container's namespaces with
nsenterto 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.
| Namespace | What it isolates | unshare option |
|---|---|---|
| PID | The 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 |
| UTS | The host name and the NIS domain name. | --uts |
| IPC | Inter-process communication: shared memory, message queues, semaphores. | --ipc |
| User | The mapping of UIDs and GIDs, hence the privileges. | --user |
| Cgroup | The view of the cgroup hierarchy, not the resource limits. | --cgroup |
| Time | The 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.
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.
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.
lsns -n -o TYPE | sort | uniq -c | sort -rn 9 mnt 7 pid 7 cgroup 6 uts 6 net 6 ipc 2 user 1 timeA 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.
-
Create the PID, Mount and Network namespaces at once
Each option requested adds a partition. The
--forkoption is mandatory with--pid: the process that callsunsharecannot 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 bashCheck 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 addrlo DOWNTerminal window ping -c1 -W1 1.1.1.1ping: connect: Network is unreachableNo route, no network card, no connectivity. It is the starting point of every container: the engine then grafts a virtual
vethinterface onto it. -
Notice that
psstill liesYou are in a new PID namespace, and yet:
Terminal window ps -p 1 -o pid,user,commPID USER COMMAND1 root systemdTerminal window ps aux | wc -l696psshows the host'ssystemdas 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 $$1The shell is PID 1 of its namespace. It is
psthat gets it wrong, because it reads the/procinherited from the host:--mountcreated a mount namespace, but it copied the existing mount table,/procincluded. -
Remount
/procso thatpstells the truthThe
mountcommand with theproctype replaces the inherited/procwith a pseudo file system fed by the current PID namespace:Terminal window mount -t proc proc /procps auxUSER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMANDroot 1 0.0 0.0 7340 3620 ? S 18:22 0:00 bashroot 7 0.0 0.0 10884 4560 ? R 18:22 0:00 ps auxTwo 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.
-
Use the
--mount-procshortcutOnce the mechanism is understood,
unsharecan do both operations in one go. The--mount-procoption implies--mountand remounts/procautomatically:Terminal window sudo unshare --pid --net --fork --mount-proc bashps auxUSER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMANDroot 1 0.0 0.0 4328 3384 ? S 13:58 0:00 bashroot 2 0.0 0.0 7896 4036 ? R 13:58 0:00 ps auxThe shell is PID 1,
psis PID 2, and nothing else exists in this namespace. It is the form to remember for everyday use. Typeexitto 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.
# Outside the namespaceiduid=1000(bob) gid=1000(bob) groups=1000(bob),27(sudo),990(docker)unshare --user --map-root-user bashiduid=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.
cat /proc/self/uid_map 0 1000 1UID 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:
touch /etc/preuve-usernstouch: cannot touch '/etc/preuve-userns': Permission deniedRoot 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:
unshare --net --fork ip -brief addrlo DOWNAn 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.
hostname # master1sudo unshare --uts --fork bashhostname conteneur-webhostname # conteneur-webexithostname # master1, unchangedThe IPC namespace partitions inter-process communication objects: message queues, semaphores, shared memory segments. Two containers cannot tread on each other's toes, and an object created in the namespace disappears with it.
sudo unshare --ipc --fork bashipcmk -Q # Message queue id: 0ipcs -q------ Message Queues --------key msqid owner perms used-bytes messages0x38b103b1 0 root 644 0 0From the host, the same ipcs -q command shows no queue: the object is
invisible outside its namespace, which protects neighbouring
applications.
The Time namespace shifts the monotonic clocks. It is mostly used for
container migration and snapshot restoration, where a piece of software must
find the uptime it had before being moved. The --boottime option takes an
offset in seconds, seven days here.
uptime -p # up 9 hours, 48 minutessudo unshare --time --fork --boottime 604800 bashuptime -pup 1 week, 9 hours, 48 minutesThe current time, on the other hand, is not changed: date returns the
same value on both sides, since only CLOCK_MONOTONIC and CLOCK_BOOTTIME are
shifted.
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.
sudo ip netns add labnsip netns listsudo ip netns exec labns ip -brief addrlabnslo DOWNThe 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:
sudo ip netns exec labns ip link set lo upsudo ip netns exec labns ping -c1 127.0.0.11 packets transmitted, 1 received, 0% packet loss, time 0msA persistent namespace must be deleted explicitly, otherwise it stays in place until the next reboot. Get into the habit of cleaning up:
sudo ip netns delete labnsip netns listThe 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:
docker run -d --name mon-conteneur \ nginx:1.29.8-alpine@sha256:5616878291a2eed594aee8db4dade5878cf7edcb475e59193904b198d9b830dePID=$(docker inspect -f '{{.State.Pid}}' mon-conteneur)sudo lsns -p "$PID" NS TYPE NPROCS PID USER COMMAND4026531834 time 745 1 root /sbin/init4026531837 user 738 1 root /sbin/init4026533214 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.
| Type | Host (PID 1) | nginx container | Isolated? |
|---|---|---|---|
| mnt | mnt:[4026531841] | mnt:[4026533214] | yes |
| uts | uts:[4026531838] | uts:[4026533215] | yes |
| ipc | ipc:[4026531839] | ipc:[4026533216] | yes |
| pid | pid:[4026531836] | pid:[4026533218] | yes |
| net | net:[4026531840] | net:[4026533220] | yes |
| cgroup | cgroup:[4026531835] | cgroup:[4026533219] | yes |
| user | user:[4026531837] | user:[4026531837] | no, shared |
| time | time:[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:
podman run --rm docker.io/library/nginx:1.29.8-alpine@sha256:5616878291a2eed594aee8db4dade5878cf7edcb475e59193904b198d9b830de iduid=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 nginxThe 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:
podman unshare cat /proc/self/uid_map 0 1000 1 1 100000 65536Enter 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.
sudo nsenter --target "$PID" --mount --uts --ipc --pid --net --cgroup -- shYou get the equivalent of a docker exec, with the container's host name,
file system and process table:
# hostname2e17ffa2b5a9# ps -efPID USER TIME COMMAND 1 root 0:00 nginx: master process nginx -g daemon off; 30 nginx 0:00 nginx: worker processThe 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:
sudo nsenter --target "$PID" --net -- ip -brief addrsudo nsenter --target "$PID" --net -- ss -tlnlo UNKNOWN 127.0.0.1/8 ::1/128eth0@if356 UP 172.17.0.3/16
State Recv-Q Send-Q Local Address:Port Peer Address:PortLISTEN 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:
sudo cat /proc/$PID/cgroup0::/system.slice/docker-2e17ffa2b5a9a90bc24fac62ecce9da06ebc481b11ecf57b3d563643d4bbdf0d.scopeSeen from inside the container, the same cgroup shows up like this:
docker exec mon-conteneur cat /proc/self/cgroup0::/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.
docker exec mon-conteneur cat /sys/fs/cgroup/memory.maxmaxThe 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:
docker run -d --name conteneur-limite --memory=64m --cpus=0.5 \ nginx:1.29.8-alpine@sha256:5616878291a2eed594aee8db4dade5878cf7edcb475e59193904b198d9b830dedocker exec conteneur-limite cat /sys/fs/cgroup/memory.maxdocker exec conteneur-limite cat /sys/fs/cgroup/cpu.max6710886450000 100000Both 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:
docker rm -f mon-conteneur conteneur-limitesudo ip netns delete labns 2>/dev/nullip netns listTroubleshooting
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.
| Symptom | Cause | Solution |
|---|---|---|
ps aux shows every host process in a PID namespace | /proc is the host's, inherited by the mount namespace | Run mount -t proc proc /proc in the namespace, or start unshare with --mount-proc |
| The "isolated" shell sees every host network interface | The --net option was forgotten: no network namespace was created | Add --net and check with readlink /proc/self/ns/net |
unshare: write failed /proc/self/uid_map: Operation not permitted | Ubuntu 23.10+ restricts unprivileged user namespaces through AppArmor | Check sysctl kernel.apparmor_restrict_unprivileged_userns and set it to 0 for a lab |
bash: fork: Cannot allocate memory after unshare --pid without --fork | The calling process did not enter the new PID namespace | Always pair --fork with --pid |
ip -brief addr fails in an nsenter --mount | You are using the image's ip, often a BusyBox one without the -brief option | Join only --net to keep the host's tools |
nsenter: cannot open /proc/<pid>/ns/pid: No such file or directory | The target process is dead, or you are not root | Reread 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. --forkis mandatory with--pid: only the process's children are born in the new PID namespace.- As long as
/procis not remounted,psshows 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
userandtimenamespaces: 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> --netis 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
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
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Understanding Linux capabilities
A new full quiz with random questions
Work again only on the questions you missed
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.