namespaces: visibility boundaries
namespaces: how the kernel hides the rest of the system from a process.
A container looks like a separate machine: its own processes, its own hostname, its own network. In reality it's an ordinary Linux process, and the kernel shows it only part of the system. The mechanism behind this is called a namespace. Let's go through the namespace types, how to see them, and how Docker uses them.
A container as seen from the host
From the host OS's point of view, there's nothing special about a container. Start a container and look at the processes on the host: you'll find an ordinary process with an ordinary PID, right next to sshd and everything else.
$ docker run -d --name web nginx:1.27.3-alpine
$ ps aux | grep nginx
root 28714 0.0 0.1 10620 6100 Ss 11:02 0:00 nginx: master process nginx -g daemon off;
That same process, seen from inside the container, sees itself as number 1. One process, two different views of it. The difference comes from the namespace.
On macOS and Windows you can't reproduce this directly. Docker Desktop runs
containers inside a small Linux virtual machine, so ps aux on the host won't
show nginx: the process lives inside that VM. To get its PID inside the VM,
use docker inspect -f '{{.State.Pid}}' web.
What a namespace is
A namespace is a kernel mechanism that limits what part of the system a process can see. There's one kernel for the whole machine, but it can show a process a restricted picture: only its own processes, only its own network interfaces, only its own directory tree.
Processes in the same namespace see the same resources. A process in a
different namespace of the same type doesn't see those resources at all.
lsns helps you check which namespaces a process sits in.
$ lsns -p 1
NS TYPE NPROCS PID USER COMMAND
4026531835 cgroup 142 1 root /sbin/init
4026531836 pid 142 1 root /sbin/init
4026531840 net 140 1 root /sbin/init
Each line describes a separate namespace with its own numeric identifier. Two processes with the same net identifier sit in the same network environment.
This is about the Linux namespace. Kubernetes has its own namespace, a partition of the cluster for objects. Same name, different meaning.
Namespace types
Each type is responsible for its own class of resources.
| Type | What it isolates |
|---|---|
| PID | process numbers; the first process inside gets PID 1 |
| Network | network interfaces, addresses, ports, routing tables |
| Mount | the tree of mounted directories |
| UTS | hostname and domain name |
| IPC | shared memory and message queues |
| User | uid and gid mapping |
| cgroup | visibility of its own cgroup hierarchy |
| Time | offset for the CLOCK_MONOTONIC and CLOCK_BOOTTIME clocks (kernel 5.6+) |
By default Docker creates its own PID, Network, Mount, UTS, and IPC namespaces
for a container. The User namespace is enabled by a separate daemon setting
(userns-remap). On a host with cgroup v2, a container also gets its own
cgroup namespace.
The PID namespace explains the trick with the first process. Inside its own PID namespace, the container's process sees numbering starting from one, while outside it has a number like 28714.
$ sudo unshare --pid --fork --mount-proc bash
# ps -e
PID TTY TIME CMD
1 pts/0 00:00:00 bash
2 pts/0 00:00:00 ps
The User namespace has the most visible effect. Inside, a process can be root with uid 0, while on the host that same process maps to an ordinary unprivileged user. That way a container's root doesn't get power over the whole machine.
$ id # on the host
uid=1000(deploy) gid=1000(deploy)
$ unshare --user --map-root-user id # inside the new user namespace
uid=0(root) gid=0(root)
On Ubuntu 23.10 and 24.04 this command fails with a permission error without sudo: unprivileged user namespaces are restricted there by AppArmor profiles. Check it and work around it:
$ sysctl kernel.apparmor_restrict_unprivileged_userns
kernel.apparmor_restrict_unprivileged_userns = 1
$ sudo unshare --user --map-root-user id
uid=0(root) gid=0(root)
A value of 1 means the restriction is on. With sudo the command works on any distribution, so the examples below just use sudo from the start.
Where a process's namespaces live
lsns isn't everywhere: minimal container images usually don't have it. The
kernel exposes any process's namespaces in the /proc/<PID>/ns directory. It
holds a symlink for each type, and the link target contains the identifier.
$ docker run --rm busybox:1.36.1 ls -l /proc/self/ns
lrwxrwxrwx ... cgroup -> cgroup:[4026532371]
lrwxrwxrwx ... ipc -> ipc:[4026532309]
lrwxrwxrwx ... mnt -> mnt:[4026532307]
lrwxrwxrwx ... net -> net:[4026532311]
lrwxrwxrwx ... pid -> pid:[4026532310]
...
lrwxrwxrwx ... uts -> uts:[4026532308]
Your numbers will be different. /proc/self points to whichever process is
reading the directory, here that's ls.
To get a single value, readlink /proc/self/ns/net is enough. Two processes
with the same number in brackets sit in the same namespace of that type.
docker exec doesn't create new namespaces. It runs the command inside the
namespaces of an already running container, so
docker exec web readlink /proc/self/ns/net shows the network namespace of
the web container itself.
Who creates namespaces
Namespaces are created by the clone and unshare system calls. clone
spawns a new process directly inside new namespaces, unshare detaches an
already running process from its current namespaces. A third call, setns,
moves a process into a namespace that already exists: that's how docker exec
works. The unshare command-line tool lets you create namespaces without
writing code.
sudo unshare --net bash # new network namespace: its own interfaces
sudo unshare --uts bash # its own hostname, change it without affecting the host
The network and UTS namespaces require root privileges on their own, so sudo
is mandatory here. Without it, they can only be created together with
--user, and that path is blocked by the same AppArmor restriction on Ubuntu
23.10 and 24.04.
Inside such a network namespace the interface list is empty except for the local loopback:
# ip link
1: lo: <LOOPBACK> mtu 65536 ...
Docker and other container runtimes do exactly this under the hood: they create a set of namespaces and start the application process inside it.
A namespace can not only be created, but also shared. Docker can start a
container inside another container's network namespace with the
--network container:<name> flag. Then the two containers share one localhost and the
same ports. This is how a Pod works in Kubernetes: its containers share a
network namespace. Similar modes exist for other types too, described in the
docker container run
reference.
Common mistake: "a container is a lightweight virtual machine"
A virtual machine carries its own kernel and emulates hardware. A container carries no kernel: all containers and the host itself run on the very same kernel. Namespaces only restrict visibility, but a process's code runs on the same kernel as the host's processes.
Easy to verify. The kernel version inside a container matches the host's, because it's literally the same one.
$ uname -r # on the host
6.8.0-31-generic
$ docker run --rm nginx:1.27.3-alpine uname -r
6.8.0-31-generic
On macOS and Windows a container will show the kernel of Docker Desktop's own
Linux VM (for example, 5.15.49-linuxkit), not your OS's kernel. The kernel
is still shared, just shared with the VM.
Resource isolation (how much CPU and memory a process gets) is handled separately, through cgroups, not namespaces. Namespaces define what a process can see. How many resources it gets is decided by cgroups.
More common mistakes
- Looking for the container's process via
pson macOS. It isn't there: it's inside the Docker Desktop VM. - Assuming
docker execstarts something "alongside" the container. The command lands in the same namespaces as the container's main process. - Packing debugging tools into the application image. A debug container can be attached to the target container's namespaces, leaving the image itself minimal.
What to remember
From the host's point of view, a container stays an ordinary process with isolated namespaces and reduced privileges.
A namespace limits visibility: PID, Network, Mount, UTS, IPC, User, cgroup, Time.
Inside a PID namespace the first process gets PID 1. Inside a User namespace, root can be an unprivileged user from the outside.
They're created with the clone and unshare calls, and setns lets you
join an existing namespace. You can see the current ones via lsns or
/proc/<PID>/ns.
A container runs on the host's kernel, it has no separate OS of its own. cgroups are responsible for limiting resources.
What you'll need for the tasks
docker run -d --name … --memory … --cpus … image:tag commanddocker exec <name> readlink /proc/self/ns/<type>to compare namespacesdocker exec <name> psanddocker exec <name> wget -qO- http://localhost/docker inspect <name>and theHostConfig.NetworkMode,HostConfig.PidModefields- the docker container run reference:
the
--networkand--pidflags
In the next lesson we'll cover cgroups: how the kernel allocates CPU and memory to a process and keeps it from eating more.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson linux-isolation-01-namespaces
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress