Sign in
← Linux: isolation
12 min · medium · Lesson 1 of 4

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.

TypeWhat it isolates
PIDprocess numbers; the first process inside gets PID 1
Networknetwork interfaces, addresses, ports, routing tables
Mountthe tree of mounted directories
UTShostname and domain name
IPCshared memory and message queues
Useruid and gid mapping
cgroupvisibility of its own cgroup hierarchy
Timeoffset 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

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

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.

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.

Get access for $49

Tell the agent: use MCP izi, give me lesson linux-isolation-01-namespaces