Linux Namespaces Explained: How They Work With Practical Examples

Containers aren’t magic. They’re ordinary Linux processes with a limited view of the system. Here’s how that works, with commands you can run yourself.


The first time I ran ps aux inside a Docker container, I got two lines of output.

PID   USER     COMMAND
    1 root     nginx: master process nginx -g daemon off;
   29 nginx    nginx: worker process

The host machine had more than three hundred processes running. The container could see two, and it believed nginx was PID 1, the process that normally belongs to init or systemd.

Nothing was virtualized. There was no hypervisor and no second kernel. That nginx process was running on the same kernel as everything else on the machine. It simply had a different view of the system.

That view comes from Linux namespaces.

If you work with Docker, Podman, Kubernetes, systemd services, or even Chrome and Flatpak, you use namespaces every day, often without noticing. In this article we’ll take them apart one at a time, using only standard Linux tools, and finish by building a tiny container by hand.

You’ll need a Linux machine or VM where you have sudo. Every command below uses util-linux and iproute2, which almost every distribution ships.


The One-Sentence Definition

A namespace wraps a global system resource so that processes inside the namespace see their own isolated copy of it.

“Global resource” here means things like the process ID list, the hostname, network interfaces, the mount table, and user IDs. Normally there is one of each for the whole machine. A namespace lets a group of processes have their own version of it.

The important part is what namespaces don’t do:

See also: Mastering the Linux Command Line — Your Complete Free Training Guide

  • They don’t limit how much CPU or memory a process uses. That’s the job of cgroups.
  • They don’t create a separate kernel. Every namespace shares the host kernel.
  • On their own, they aren’t a full security boundary. Container runtimes add capabilities, seccomp, and AppArmor or SELinux on top.

Namespaces control what a process can see. Cgroups control how much it can use. A container is roughly both combined, plus a root filesystem.


The Eight Namespaces

Linux currently has eight namespace types:

NamespaceFlagIsolatesAdded in kernel
MountCLONE_NEWNSMount points / filesystem tree2.4.19 (2002)
UTSCLONE_NEWUTSHostname and NIS domain name2.6.19
IPCCLONE_NEWIPCSystem V IPC, POSIX message queues2.6.19
PIDCLONE_NEWPIDProcess IDs2.6.24
NetworkCLONE_NEWNETInterfaces, IPs, routes, firewall rules, ports2.6.29
UserCLONE_NEWUSERUser and group IDs, capabilities3.8
CgroupCLONE_NEWCGROUPView of the cgroup hierarchy4.6
TimeCLONE_NEWTIMEMonotonic and boot-time clocks5.6

The mount namespace was the first one, and at the time nobody expected there to be more. That’s why its flag is the oddly generic CLONE_NEWNS (“new namespace”) instead of CLONE_NEWMNT.


Every Process Is Already in Namespaces

A common misconception is that namespaces are something only containers “have”. In reality every process on Linux belongs to exactly one namespace of each type, all the time. Processes that were never isolated simply share the initial, or “root”, namespaces with PID 1.

You can see which namespaces your shell is in:

ls -l /proc/$$/ns
lrwxrwxrwx 1 alice alice 0 Sep 26 10:02 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 alice alice 0 Sep 26 10:02 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 alice alice 0 Sep 26 10:02 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 alice alice 0 Sep 26 10:02 net -> 'net:[4026531840]'
lrwxrwxrwx 1 alice alice 0 Sep 26 10:02 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 alice alice 0 Sep 26 10:02 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 alice alice 0 Sep 26 10:02 time -> 'time:[4026531834]'
lrwxrwxrwx 1 alice alice 0 Sep 26 10:02 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 alice alice 0 Sep 26 10:02 user -> 'user:[4026531837]'
lrwxrwxrwx 1 alice alice 0 Sep 26 10:02 uts -> 'uts:[4026531838]'

$$ is the PID of your current shell. The number in brackets is an inode number that identifies the namespace. Two processes with the same number for net share the same network stack. Different numbers mean different network stacks.

That gives you a simple way to compare any two processes:

sudo readlink /proc/1/ns/net /proc/$$/ns/net
net:[4026531840]
net:[4026531840]

Same number, same network namespace.

For a system-wide overview, use lsns:

sudo lsns
        NS TYPE   NPROCS   PID USER    COMMAND
4026531834 time      212     1 root    /sbin/init
4026531835 cgroup    212     1 root    /sbin/init
4026531836 pid       212     1 root    /sbin/init
4026531837 user      211     1 root    /sbin/init
4026531838 uts       205     1 root    /sbin/init
4026531839 ipc       212     1 root    /sbin/init
4026531840 net       208     1 root    /sbin/init
4026531841 mnt       196     1 root    /sbin/init
4026532214 mnt         1   612 root    /usr/lib/systemd/systemd-udevd
4026532215 uts         1   612 root    /usr/lib/systemd/systemd-udevd
4026532301 net         1   845 root    /usr/sbin/chronyd
...

Even on a machine with no containers, you’ll often find systemd services in their own mount, UTS, or network namespaces. That’s what settings like PrivateTmp=yes, PrivateNetwork=yes, and ProtectHostname=yes do under the hood.


The Three System Calls (and Their Command-Line Twins)

The kernel interface is small:

System callWhat it doesCommand-line tool
clone()Create a new process in new namespacesused by container runtimes
unshare()Move the calling process into new namespacesunshare
setns()Join an existing namespacensenter, ip netns exec

We’ll mostly use unshare to create namespaces and nsenter to step into existing ones.

Now let’s go through them one at a time, starting with the easiest.


1. UTS Namespace: Your Own Hostname

UTS stands for “UNIX Time-Sharing”, a historical name. Today it just means hostname and domain name.

Open a shell in a new UTS namespace:

sudo unshare --uts bash

Change the hostname:

hostname container-demo
hostname
container-demo

Now open a second terminal on the host and check:

hostname
my-laptop

The host is untouched. Only processes in the new UTS namespace see container-demo.

Your prompt probably still shows the old name, because bash read the hostname when it started. Run exec bash to refresh it.

Type exit to leave. As soon as the last process in a namespace exits, the kernel destroys the namespace.

This is why every Docker container has its own hostname, usually its container ID.


2. PID Namespace: Becoming PID 1

This is the one that surprised me with nginx.

sudo unshare --pid --fork --mount-proc bash

Now run:

ps aux
USER   PID %CPU %MEM    VSZ   RSS TTY   STAT START   TIME COMMAND
root     1  0.0  0.0   8928  5500 pts/2 S    10:15   0:00 bash
root     8  0.0  0.0  10620  3300 pts/2 R+   10:15   0:00 ps aux

Your bash shell is PID 1, and nothing else on the machine exists as far as this shell is concerned.

Each option does a specific job:

  • --pid creates a new PID namespace.
  • --fork is needed because of a quirk: after unshare(), the calling process stays in its old PID namespace, and only its children go into the new one. --fork makes unshare start bash as a child so bash becomes PID 1. Without it you’ll see strange errors like fork: Cannot allocate memory after your first command.
  • --mount-proc mounts a fresh /proc. ps doesn’t ask the kernel for a process list directly. It reads /proc. If you skip this option, ps keeps reading the host’s /proc and you’ll see every host process, which confuses a lot of people the first time.

The same process, two PIDs

While that shell is running, find it from a host terminal:

pgrep -f "unshare --pid" 
ps -ef --forest | grep -A1 "unshare --pid"
root   23456  23455  0 10:15 pts/2  00:00:00 unshare --pid --fork --mount-proc bash
root   23457  23456  0 10:15 pts/2  00:00:00  \_ bash

The host calls it PID 23457. Ask the kernel for both identities:

grep NSpid /proc/23457/status
NSpid:  23457   1

It’s one process with two PIDs: 23457 in the parent namespace and 1 in the child. PID namespaces are nested. A parent can see and signal every process in its child namespaces, but a child can’t see its parent’s processes at all.

Why PID 1 matters

PID 1 has special duties. It adopts orphaned processes and must reap zombies. If PID 1 in a namespace exits, the kernel kills every other process in that namespace.

That’s why a container stops when its main process exits, and why tools like tini or docker run --init exist. A regular application running as PID 1 often doesn’t reap zombies, and it may also ignore SIGTERM because PID 1 doesn’t get the default signal handlers.


3. Mount Namespace: A Private Filesystem View

A mount namespace gives processes their own copy of the mount table, the list you see in findmnt.

sudo unshare --mount bash

Mount something:

mount -t tmpfs tmpfs /mnt
echo "hello from the namespace" > /mnt/test.txt
findmnt /mnt
TARGET SOURCE FSTYPE OPTIONS
/mnt   tmpfs  tmpfs  rw,relatime,inode64

In another host terminal:

findmnt /mnt
ls /mnt

No output. The host doesn’t see the tmpfs, and the file isn’t there.

When the new namespace is created, it gets a copy of the parent’s mount table. After that, the two can diverge.

The propagation trap

There’s a detail that bites people when they call unshare() themselves instead of using the command-line tool.

systemd marks the root filesystem as shared:

findmnt -o TARGET,PROPAGATION /
TARGET PROPAGATION
/      shared

With shared propagation, mount events can leak between namespaces, so a mount you make inside might show up on the host. The unshare command avoids this by setting propagation to private inside the new namespace by default (--propagation private). If you write code with the raw unshare(CLONE_NEWNS) system call, you have to do this yourself:

mount --make-rprivate /

Container runtimes build on the mount namespace. They create one, mount the image’s filesystem, and then use pivot_root to make it the new /.


4. Network Namespace: A Whole Separate Network Stack

This one is the most useful to understand, because most container networking problems come down to it.

A network namespace has its own:

  • network interfaces
  • IP addresses
  • routing table
  • iptables or nftables rules
  • socket and port space, so two namespaces can both listen on port 80
  • /proc/sys/net settings

Look at an empty one:

sudo unshare --net bash
ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

There’s only a loopback interface, and it’s down. There’s no eth0, no route, and no way out:

ping -c1 8.8.8.8
ping: connect: Network is unreachable

That’s the starting point for every container. Type exit, and let’s wire one up properly.

Connecting a namespace with a veth pair

ip netns creates named network namespaces, which are easier to work with:

sudo ip netns add red
ip netns list
red

A veth pair works like a virtual network cable with two ends. Whatever goes in one end comes out the other. Create one and put one end into red:

sudo ip link add veth-host type veth peer name veth-red
sudo ip link set veth-red netns red

Give both ends an address and bring them up:

sudo ip addr add 10.200.0.1/24 dev veth-host
sudo ip link set veth-host up

sudo ip netns exec red ip addr add 10.200.0.2/24 dev veth-red
sudo ip netns exec red ip link set veth-red up
sudo ip netns exec red ip link set lo up

Test it:

ping -c2 10.200.0.2
PING 10.200.0.2 (10.200.0.2) 56(84) bytes of data.
64 bytes from 10.200.0.2: icmp_seq=1 ttl=64 time=0.061 ms
64 bytes from 10.200.0.2: icmp_seq=2 ttl=64 time=0.048 ms

From inside the namespace:

sudo ip netns exec red ip route
10.200.0.0/24 dev veth-red proto kernel scope link src 10.200.0.2

The host and the namespace are now connected, but the namespace only knows about 10.200.0.0/24. To reach the internet, you’d add a default route inside the namespace, enable IP forwarding on the host, and add a NAT rule.

That’s essentially what Docker does. It creates a docker0 bridge, connects one veth end per container to it, and uses a MASQUERADE rule for outbound traffic. Kubernetes CNI plugins do a more elaborate version of the same thing.

Proving ports are isolated

Start a web server in the namespace:

sudo ip netns exec red python3 -m http.server 80 &

Start another one on the host, on the same port:

sudo python3 -m http.server 80 &

There’s no Address already in use error. Each namespace has its own port 80. From the host:

curl -s -o /dev/null -w "%{http_code}\n" http://10.200.0.2/
200

Clean up:

sudo kill %1 %2
sudo ip netns delete red

Deleting the namespace destroys veth-red, and when one end of a veth pair disappears the kernel removes the other end too. veth-host is gone as well.


5. IPC Namespace: Isolated Shared Memory and Queues

Processes can talk to each other with System V IPC objects (shared memory segments, semaphores, message queues) and POSIX message queues. These are identified by global keys, which is a problem if two unrelated applications pick the same key.

Create a message queue on the host:

ipcmk -Q
ipcs -q
Message queue id: 0

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x5f2e1a3c 0          alice      644        0            0

Now look from a new IPC namespace:

sudo unshare --ipc ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

It’s empty. Clean up the host queue with ipcrm -q 0.

You rarely think about this namespace, but databases like PostgreSQL and Oracle historically relied on System V shared memory. Without IPC isolation, two containers could collide or even read each other’s segments.

In Kubernetes, all containers in a Pod share an IPC namespace, which is how sidecars can use shared memory with the main container.


6. User Namespace: Root That Isn’t Root

The user namespace is the most powerful one, and it’s the foundation of rootless containers.

A user namespace has its own mapping of user and group IDs. A process can be UID 0 inside the namespace while being an ordinary user outside.

Try it without sudo:

id
uid=1000(alice) gid=1000(alice) groups=1000(alice),27(sudo)
unshare --user --map-root-user bash
id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)

You became “root” without a password. Before you worry, look at the mapping:

cat /proc/self/uid_map
         0       1000          1

The three columns mean: UID inside, UID outside, how many IDs. So UID 0 here is UID 1000 on the host, and only one ID is mapped.

Now try something only real root can do:

cat /etc/shadow
cat: /etc/shadow: Permission denied

The kernel checks host file access against your real UID, 1000. Your “root” powers only apply to resources owned by this user namespace.

Groups the kernel couldn’t map show up as nogroup (65534). That’s why you saw 65534(nogroup) in the id output.

Why this matters: unprivileged namespaces

Creating most namespaces requires CAP_SYS_ADMIN, which normally means root. But inside a user namespace you do have CAP_SYS_ADMIN over that namespace. So you can create the others without sudo:

unshare --user --map-root-user --net --uts --mount --pid --fork --mount-proc bash
hostname sandbox
ip addr
ps aux

Everything works, and at no point did you use sudo. This is how rootless Podman, rootless Docker, browser sandboxes, and Flatpak isolate processes without root privileges.

For real containers, a single mapped UID isn’t enough, because images contain files owned by many UIDs. That’s where /etc/subuid and /etc/subgid come in:

grep "^$(whoami):" /etc/subuid
alice:100000:65536

This lets alice map 65,536 additional IDs (100000 to 165535) into her user namespaces. Container root becomes host UID 100000, container UID 33 becomes 100033, and so on.

When it doesn’t work

Unprivileged user namespaces have exposed a lot of kernel attack surface over the years, so some distributions restrict them:

sysctl user.max_user_namespaces
sysctl kernel.apparmor_restrict_unprivileged_userns   # Ubuntu 23.10+

If the first is 0, or the second is 1 and no AppArmor profile allows it, unshare --user fails with an error like:

unshare: write failed /proc/self/uid_map: Operation not permitted

7. Cgroup Namespace: Hiding the Cgroup Path

Cgroups limit resources. The cgroup namespace is about what a process sees when it looks at its own cgroup.

On the host:

cat /proc/self/cgroup
0::/user.slice/user-1000.slice/session-3.scope

In a new cgroup namespace:

sudo unshare --cgroup cat /proc/self/cgroup
0::/

The process now sees its current cgroup as the root. It can’t see, or learn anything about, the host’s hierarchy above it.

This matters for two reasons:

  • Information hiding. A container shouldn’t learn paths like /kubepods.slice/kubepods-burstable.slice/... that reveal details about the host.
  • Portability. Software that manages cgroups, such as systemd running inside a container, expects to be at the root of the tree.

Docker and Podman enable a cgroup namespace by default on cgroup v2 hosts.


8. Time Namespace: A Different Uptime

The newest namespace, added in Linux 5.6, lets processes see different values for CLOCK_MONOTONIC and CLOCK_BOOTTIME.

uptime -p
up 3 hours, 12 minutes
sudo unshare --time --fork --boottime 864000 uptime -p
up 1 week, 3 days, 3 hours, 12 minutes

We added ten days (864,000 seconds) to the boot time clock for that one process.

What it can’t change is wall-clock time (CLOCK_REALTIME). date returns the same result everywhere. Isolating real time would break too many things, such as TLS certificate checks and log timestamps.

The main user is checkpoint/restore (CRIU). When a container is frozen on one machine and restored on another, its monotonic clock must not suddenly jump. The time namespace lets the restored processes keep seeing continuous values.

Like the PID namespace, it applies to children of the caller, which is why --fork is needed.


Joining an Existing Namespace with nsenter

Creating namespaces is half the story. In real troubleshooting you usually need to enter one that already exists, and that’s where nsenter pays off.

Here’s a situation that comes up constantly. You have a container built from a minimal image, and it has no ip, ss, curl, or even a shell:

docker run -d --name web nginx
docker exec web ip addr
OCI runtime exec failed: exec failed: unable to start container process:
exec: "ip": executable file not found in $PATH

But the container is just a process. Find its host PID:

PID=$(docker inspect -f '{{.State.Pid}}' web)
echo $PID
48213

Check its namespaces:

sudo lsns -p $PID
        NS TYPE   NPROCS   PID USER COMMAND
4026531834 time      215     1 root /sbin/init
4026531837 user      214     1 root /sbin/init
4026532410 mnt         3 48213 root nginx: master process nginx -g daemon off;
4026532411 uts         3 48213 root nginx: master process nginx -g daemon off;
4026532412 ipc         3 48213 root nginx: master process nginx -g daemon off;
4026532413 pid         3 48213 root nginx: master process nginx -g daemon off;
4026532415 net         3 48213 root nginx: master process nginx -g daemon off;
4026532480 cgroup      3 48213 root nginx: master process nginx -g daemon off;

You can read a lot from this. The container has its own mount, UTS, IPC, PID, network, and cgroup namespaces. It shares the host’s user and time namespaces, which means root in this container is real host root (Docker’s default, unless you enable userns-remap).

Now enter only the network namespace, and use the host’s own tools:

sudo nsenter --target $PID --net ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 ...
    inet 127.0.0.1/8 scope host lo
2: eth0@if52: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
sudo nsenter -t $PID -n ss -tlnp
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN 0      511          0.0.0.0:80         0.0.0.0:*     users:(("nginx",pid=48213,fd=6))

You’re using the host’s ip and ss binaries, because you didn’t enter the container’s mount namespace, while looking at the container’s network. This trick works for Kubernetes Pods too: find the container PID with crictl inspect, then nsenter into it.

Notice eth0@if52. The @if52 means the other end of this veth pair is interface index 52 on the host:

ip link | grep "^52:"
52: veth3a9f1c2@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master docker0 ...

That’s the “virtual cable” from earlier, plugged into the docker0 bridge.

To enter all of a container’s namespaces, which is roughly what docker exec does:

sudo nsenter -t $PID --all /bin/sh

How Long Does a Namespace Live?

Normally a namespace disappears when its last process exits. There are two ways to keep one alive with no processes inside:

  1. Hold an open file descriptor to /proc/<pid>/ns/<type>.
  2. Bind-mount that file somewhere else.

ip netns add uses the second method:

sudo ip netns add blue
findmnt -t nsfs
TARGET          SOURCE                  FSTYPE OPTIONS
/run/netns/blue nsfs[net:[4026532520]]  nsfs   rw

That bind mount is the only thing keeping the blue namespace alive. ip netns delete blue just unmounts it.

Kubernetes uses a related trick. Every Pod starts with a tiny pause container whose only job is to sit there and hold the Pod’s network and IPC namespaces. Your application containers then join those namespaces. If an app container crashes and restarts, the Pod keeps its IP address because the namespace never went away.


Putting It Together: A Container in Five Commands

Now let’s combine everything into a container built by hand, with no Docker involved at runtime.

First we need a root filesystem. The easiest source is an existing image:

mkdir -p ~/minibox
docker export $(docker create alpine) | tar -x -C ~/minibox
ls ~/minibox
bin  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

(If you don’t have Docker, download an Alpine “minirootfs” tarball from alpinelinux.org and extract it there instead.)

Now launch a shell with new PID, mount, UTS, IPC, and network namespaces, and switch its root to that directory:

sudo unshare --pid --fork --mount --uts --ipc --net \
  chroot ~/minibox /bin/sh -c '
    mount -t proc proc /proc
    hostname minibox
    exec /bin/sh
  '

Look around:

/ # cat /etc/os-release | head -1
NAME="Alpine Linux"

/ # hostname
minibox

/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    5 root      0:00 ps

/ # ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

It’s an Alpine userland on your host kernel, with its own hostname, its own process tree starting at PID 1, and an empty network stack.

Check the kernel version to prove it:

/ # uname -r
6.8.0-45-generic

That’s the host’s kernel. There’s only ever one.

What this toy is missing compared to a real runtime such as runc:

  • cgroups: no CPU or memory limits
  • pivot_root instead of chroot, since chroot is easier to escape
  • capability dropping: this shell has full root capabilities
  • seccomp: every system call is allowed
  • a user namespace: root inside is real root outside
  • networking: no veth pair, bridge, or NAT
  • AppArmor or SELinux profiles

Each of those is a layer that runc, crun, and friends add. But the core idea, the thing that makes a process feel like it’s in its own machine, is just the namespaces you’ve been creating by hand.

Type exit to leave. PID 1 exits, the kernel tears down every namespace, and nothing is left behind.


What Namespaces Don’t Isolate

Namespaces are powerful, but there’s only one kernel, and several things stay global:

  • The kernel itself. A kernel bug is exploitable from any namespace. This is the biggest reason containers are a weaker boundary than VMs.
  • Kernel modules. lsmod shows the same list everywhere, and a module loaded for one container affects all of them.
  • Wall-clock time. CLOCK_REALTIME isn’t namespaced.
  • Most kernel.* sysctls. net.* settings are per network namespace, but many others are global.
  • Devices. Access to /dev entries is controlled by the device cgroup and by what gets mounted, not by a “device namespace”.
  • Resource usage. A process in its own namespaces can still eat all the memory unless cgroups limit it.
  • /proc and /sys leaks. Files such as /proc/meminfo and /proc/cpuinfo show host-wide values unless something like LXCFS is used. This is why free inside a container often reports the host’s total memory.

Quick Reference

# See your current namespaces
ls -l /proc/$$/ns

# List all namespaces on the system
sudo lsns
sudo lsns -t net

# Namespaces of a specific process
sudo lsns -p <PID>

# Compare two processes
sudo readlink /proc/<PID1>/ns/net /proc/<PID2>/ns/net

# Create namespaces
sudo unshare --uts bash
sudo unshare --pid --fork --mount-proc bash
sudo unshare --mount bash
sudo unshare --net bash
sudo unshare --ipc bash
unshare --user --map-root-user bash
sudo unshare --cgroup bash
sudo unshare --time --fork --boottime 86400 bash

# Named network namespaces
sudo ip netns add NAME
sudo ip netns exec NAME COMMAND
sudo ip netns delete NAME

# Enter a running container's namespaces
PID=$(docker inspect -f '{{.State.Pid}}' CONTAINER)
sudo nsenter -t $PID -n ip addr      # network only
sudo nsenter -t $PID --all /bin/sh   # everything

# Both PIDs of a namespaced process
grep NSpid /proc/<PID>/status

Final Thoughts

Before I understood namespaces, containers felt like a black box. Things like “the container can’t see the port,” “ps shows nothing,” or “free reports the wrong memory” seemed like random Docker behavior.

Once you know that a container is a normal process with a few namespace flags set, those problems become much easier to reason about:

  • Can’t reach a service? Check the network namespace. Is the process listening on the interface you think it is?
  • Container dies unexpectedly? Remember that PID 1 exiting kills the whole PID namespace.
  • A mount isn’t visible? Check the mount namespace and mount propagation.
  • Wondering if container root is dangerous? Check whether it has its own user namespace.

Next time you’re debugging a container, skip docker exec for a minute and try this:

sudo nsenter -t $(docker inspect -f '{{.State.Pid}}' mycontainer) -n ss -tlnp

It shows you exactly what the container sees, using tools from your own host.


If this helped, the natural next step is cgroups, the other half of how containers work. Namespaces decide what a process can see. Cgroups decide how much it can use.

Avatar photo
David Cao

David is a Cloud & DevOps Enthusiast. He has years of experience as a Linux engineer. He had working experience in AMD, EMC. He likes Linux, Python, bash, and more. He is a technical blogger and a Software Engineer. He enjoys sharing his learning and contributing to open-source.

Articles: 726

Leave a Reply

Your email address will not be published. Required fields are marked *