docker

Linux Namespaces and Cgroups Explained: How Containers Work

Learn how Linux namespaces and cgroups v2 isolate and limit containers, with hands-on unshare, nsenter and Docker examples, cgroup files explained, and security caveats.

By Shubhankar Tripathi • • 5 min read

Linux Namespaces and Cgroups: The Foundations of Container Isolation

A container is often described as a "lightweight virtual machine", but that description hides what is really going on. There is no separate kernel and no hardware emulation. A container is an ordinary Linux process that the kernel has been told to treat differently: it sees its own process list, its own network, its own filesystem and its own hostname, and it can use only a limited share of CPU, memory and other resources.

Two Linux kernel features make this possible. Namespaces control what a process can see. Control groups (cgroups) control how much a process can use. Docker, Podman, containerd and Kubernetes all build on these same two features.

In this guide you will learn how each namespace and each important cgroup controller works, try them by hand with unshare and nsenter, see exactly how Docker flags such as --memory and --cpus map to kernel files, and understand where the isolation stops. Examples are written for Docker Engine 29 on a modern Linux distribution with cgroup v2, as of October 2026.

Quick Answer


Namespaces

Cgroups

Question answered

What can this process see?

How much can this process use?

Provides

Isolation of views: processes, network, mounts, hostname, IPC, users, cgroups, time

Limits, prioritisation and accounting for CPU, memory, process count and I/O

Without it

A container could see and signal every process on the host, share its network and filesystem

One container could consume all memory or CPU and starve every other workload

Where to look

/proc/<pid>/ns/

/sys/fs/cgroup/

Docker examples

--network, --pid, --ipc, --userns

--memory, --cpus, --pids-limit


Figure 1: Namespaces control what a container sees; cgroups control what it can use

Figure 1: Namespaces control what a container sees; cgroups control what it can use

Containers Are Just Processes

Start a container and look for it from the host. You will find it in the host's ordinary process list:

docker run -d --name web nginx:1.29

ps -eo pid,user,cmd | grep "nginx: master"

Inside the container, the same Nginx process believes it is PID 1. On the host, it has an ordinary PID such as 48213. Nothing is emulated: the kernel simply presents a different view to that process (namespaces) and enforces limits on it (cgroups). Ask Docker for the host PID directly:

docker inspect -f "{{.State.Pid}}" web

We will use this PID throughout the guide to explore the container from the host.

Part 1: Linux Namespaces

A namespace wraps a global system resource so that processes inside the namespace get their own isolated instance of it. Processes in different namespaces of the same type cannot see each other's instance. Linux currently has eight namespace types:

Namespace

Kernel flag

Isolates

Added in kernel

Mount (mnt)

CLONE_NEWNS

Mount points: the filesystem tree a process sees

2.4.19 (2002)

UTS (uts)

CLONE_NEWUTS

Hostname and NIS domain name

2.6.19

IPC (ipc)

CLONE_NEWIPC

System V IPC objects and POSIX message queues

2.6.19

PID (pid)

CLONE_NEWPID

Process IDs: a separate process tree with its own PID 1

2.6.24

Network (net)

CLONE_NEWNET

Network interfaces, IP addresses, routing tables, firewall rules, ports

2.6.29

User (user)

CLONE_NEWUSER

User and group IDs: root inside can be unprivileged outside

3.8

Cgroup (cgroup)

CLONE_NEWCGROUP

The view of the cgroup hierarchy

4.6

Time (time)

CLONE_NEWTIME

Offsets for the monotonic and boot-time clocks

5.6


Namespaces are created and joined with three system calls: clone() creates a new process in new namespaces, unshare() moves the calling process into new namespaces, and setns() joins an existing namespace. Container runtimes such as runc use exactly these calls.

PID Namespace: A Private Process Tree

In a new PID namespace, the first process gets PID 1 and sees only its own descendants. Processes inside cannot see or signal processes outside. The host, however, can see every container process, each with its host PID.

docker run --rm alpine ps

The output lists only ps itself as PID 1. Because the first process becomes PID 1, it inherits PID 1's special responsibilities: signal handling and reaping zombie processes. Our tutorial Graceful Shutdown in Docker: Signals, PID 1 and tini covers the consequences in detail.

Network Namespace: A Private Network Stack

A new network namespace starts with only a loopback interface. It has its own interfaces, IP addresses, routing table, firewall rules and port numbers, which is why two containers can both listen on port 80. Docker connects each container to a bridge network using a virtual Ethernet (veth) pair: one end inside the container as eth0, the other end attached to the docker0 bridge on the host.

docker run --rm --network none alpine ip addr

docker run --rm alpine ip addr

The first command shows only lo; the second also shows eth0 with a private address on the bridge network. For a full explanation, see our tutorial Docker Network Architecture.

Mount Namespace: A Private Filesystem View

A mount namespace gives the process its own list of mount points. Docker uses it to mount the image's layered filesystem as the container's root (/), plus volumes, bind mounts and special filesystems such as /proc and /dev. Mounts made inside the container do not appear on the host, and the host's mounts are not visible inside unless Docker deliberately mounts them.

UTS Namespace: A Private Hostname

UTS stands for "UNIX Time-sharing System", a historical name. It isolates the hostname, so each container can have its own:

docker run --rm --hostname api-01 alpine hostname

IPC Namespace: Private Shared Memory

The IPC namespace isolates System V shared memory, semaphores and message queues, and POSIX message queues. Processes in different containers therefore cannot accidentally (or deliberately) use each other's shared memory segments. Containers that genuinely need to share memory can opt in with --ipc container:<name> or --ipc shareable.

User Namespace: Root Inside, Unprivileged Outside

A user namespace maps user and group IDs inside the namespace to different IDs outside. A process can be UID 0 (root) inside while being an unprivileged UID on the host. If it escapes the container, it has no special power on the host.

Important: Docker does not enable user namespaces for containers by default. With a default installation, root in a container is UID 0 on the host, restricted only by capabilities, seccomp and other security features. Enable user namespaces with the daemon's userns-remap option, or run the whole daemon in rootless mode; see our tutorial Rootless Docker: Setup and Limitations.

Cgroup Namespace: A Private View of Cgroups

The cgroup namespace virtualises the view of /sys/fs/cgroup, so a container sees its own cgroup as the root of the hierarchy instead of the host's full tree and its paths. On cgroup v2 hosts, Docker gives each container a private cgroup namespace by default (--cgroupns private).

Time Namespace: Private Clock Offsets

The newest namespace lets a process see different values for the monotonic and boot-time clocks. It exists mainly for checkpoint and restore, so a migrated process does not see time jump backwards. It does not change the wall-clock time, and Docker does not use it by default.

How Docker Uses Namespaces by Default

Namespace

Docker default

Share with the host

Share with another container

PID

New namespace

--pid host

--pid container:<name>

Network

New namespace on the bridge network

--network host

--network container:<name>

Mount

New namespace

Not available (use bind mounts)

Not available (use volumes)

UTS

New namespace

--uts host

Not available

IPC

New private namespace

--ipc host

--ipc container:<name>

User

Not used (shares the host's)

Default; --userns host opts out of remapping

Not available

Cgroup

New private namespace on cgroup v2

--cgroupns host

Not available

Time

Not used

Default

Not available


Security: Every "host" option above removes a layer of isolation. --pid host lets a container see and signal every host process, and --network host gives it direct access to the host's network stack. Use them only for trusted system tools that genuinely need them, such as some monitoring agents.

Hands-On: Explore Namespaces

Create Namespaces Yourself With unshare

You can build a basic container-like environment with one command, without Docker. Run this on a Linux machine (not inside a container):

sudo unshare --pid --fork --mount-proc --uts --net --ipc bash

Inside the new shell, try these commands:

hostname demo-box && hostname   # changes only in this UTS namespace

ps aux                           # bash is PID 1, nothing else is visible

ip link                          # only the loopback interface exists

Open another terminal: the host's hostname has not changed. Type exit to leave. You have just created PID, mount, UTS, network and IPC isolation by hand. This is the core of what a container runtime does, before it adds a root filesystem, cgroups and security settings.

Inspect a Container's Namespaces

Every process's namespaces appear as symbolic links in /proc/<pid>/ns/. Two processes are in the same namespace when the links point to the same inode number:

PID=$(docker inspect -f "{{.State.Pid}}" web)

sudo ls -l /proc/$PID/ns/

sudo ls -l /proc/1/ns/

Compare the numbers: the container's pid, net, mnt, uts, ipc and cgroup links differ from the host's (PID 1), while its user and time links are the same, confirming the defaults in the table above. The lsns command lists all namespaces on the system:

sudo lsns -p $PID

Enter a Container's Namespaces With nsenter

nsenter runs a command inside an existing process's namespaces. This is extremely useful for debugging minimal images that have no shell or networking tools, because you can use the host's tools inside the container's network namespace:

sudo nsenter -t $PID -n ip addr          # container network, host tools

sudo nsenter -t $PID -n ss -tlnp         # listening ports inside the container

sudo nsenter -t $PID -u hostname         # container hostname

docker exec does the same thing internally: it starts a new process that joins all of the container's namespaces.

Part 2: Control Groups (Cgroups)

Namespaces isolate what a process sees, but they do not stop it from using all the memory or CPU on the machine. Control groups organise processes into a hierarchy of groups and let the kernel limit, prioritise and account for the resources each group uses.

Cgroups are managed through a special filesystem mounted at /sys/fs/cgroup. Each group is a directory, and each setting is a file. Writing a value to a file such as memory.max sets a limit; reading a file such as memory.current reports usage.

Cgroups v1 vs Cgroups v2


cgroup v1 (legacy)

cgroup v2 (unified)

Hierarchy

A separate tree for each controller

One single, unified tree for all controllers

Consistency

Each controller has its own rules and file names

Consistent interface and file naming across controllers

Memory features

Basic limits

Adds memory.high throttling, better OOM handling and accounting

Pressure information

Not available

Pressure Stall Information (PSI) files show resource contention

Rootless containers

Resource limits not supported

Supported through systemd delegation

Status

Deprecated in Docker Engine 29.0 (removal date not yet set)

Default on all current major distributions


Ubuntu 21.10 and later, Debian 11 and later, RHEL 9 and later, and Fedora 31 and later use cgroup v2 by default. Check your system:

stat -fc %T /sys/fs/cgroup/

docker info --format "{{.CgroupVersion}} {{.CgroupDriver}}"

The first command prints cgroup2fs on cgroup v2. The second typically prints 2 systemd, meaning Docker uses cgroup v2 and lets systemd manage the container cgroups (the recommended driver).

The Important Cgroup v2 Controllers

Controller

Controls

Key files

memory

RAM and swap usage

memory.max, memory.high, memory.low, memory.swap.max, memory.current, memory.events

cpu

CPU time: hard caps and relative weights

cpu.max, cpu.weight, cpu.stat

cpuset

Which CPU cores and memory nodes may be used

cpuset.cpus, cpuset.mems

pids

Maximum number of processes and threads

pids.max, pids.current

io

Block device bandwidth and IOPS

io.max, io.weight, io.stat

hugetlb

Huge page usage

hugetlb.<size>.max


How Docker Flags Map to Cgroup Files

When you pass a resource flag to docker run, Docker (through containerd and runc) writes the corresponding value into the container's cgroup:

Docker flag

Cgroup v2 file

Example value written

--memory 512m

memory.max

536870912

--memory-reservation 256m

memory.low

268435456

--memory 512m --memory-swap 1g

memory.swap.max

536870912 (swap = total minus memory)

--cpus 1.5

cpu.max

150000 100000 (quota and period in microseconds)

--cpu-shares 512

cpu.weight

Converted from shares to a relative weight

--cpuset-cpus 0,1

cpuset.cpus

0-1

--pids-limit 200

pids.max

200

--device-read-bps /dev/sda:10mb

io.max

8:0 rbps=10485760


cpu.max works as a quota per period: 150000 100000 means the container may use 150,000 microseconds of CPU time in every 100,000-microsecond period, which equals one and a half CPU cores. When it uses up its quota, it is throttled until the next period.

Docker defaults: Without resource flags, a container has no memory or CPU limit: it can use everything the host has. If you set --memory but not --memory-swap, the container may additionally use swap equal to its memory limit (if the host has swap). Always set explicit limits in production.

Hands-On: Watch Cgroups Work

Verify a Limit From Inside the Container

Because Docker gives each container a private cgroup namespace, the container sees its own cgroup at the root of /sys/fs/cgroup:

docker run --rm --memory 256m --cpus 0.5 --pids-limit 100 alpine \

  cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max /sys/fs/cgroup/pids.max

Expected output:

268435456

50000 100000

100

Find a Container's Cgroup From the Host

With the systemd cgroup driver, each container gets a systemd scope unit named after its full ID:

ID=$(docker inspect -f "{{.Id}}" web)

ls /sys/fs/cgroup/system.slice/docker-$ID.scope/

cat /sys/fs/cgroup/system.slice/docker-$ID.scope/memory.current

memory.current shows the container's current memory usage in bytes, which is where docker stats gets its numbers.

See the OOM Killer in Action

When a container exceeds memory.max, the kernel's out-of-memory (OOM) killer terminates a process inside that cgroup. Allocate more memory than the limit allows and watch what happens:

docker run --name oom-demo --memory 100m --memory-swap 100m python:3.13-slim \

  python -c "data = bytearray(300 * 1024 * 1024)"

 

docker inspect -f "ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}" oom-demo

docker rm oom-demo

The output shows ExitCode=137 OOMKilled=true. Exit code 137 means the process was killed with SIGKILL (128 + 9), and OOMKilled=true confirms the cgroup memory limit was the cause. Setting --memory-swap equal to --memory disables swap, so the limit is hit immediately. For a full troubleshooting guide, see our tutorial How to Fix Docker Exit Code 137.

Watch CPU Throttling

docker run -d --name cpu-demo --cpus 0.5 alpine sh -c "while true; do :; done"

docker stats --no-stream cpu-demo

docker exec cpu-demo cat /sys/fs/cgroup/cpu.stat

docker rm -f cpu-demo

docker stats shows CPU usage capped at about 50%. In cpu.stat, the nr_throttled and throttled_usec values keep growing, showing how often and for how long the kernel paused the container because it had used up its quota.

Beyond Limits: Pressure Stall Information

Cgroup v2 provides Pressure Stall Information (PSI) in the files cpu.pressure, memory.pressure and io.pressure. They show the percentage of time processes in the cgroup were stalled waiting for that resource over the last 10, 60 and 300 seconds.

cat /sys/fs/cgroup/system.slice/docker-$ID.scope/memory.pressure

PSI is a far better early-warning signal than raw usage: a container at 90% of its memory limit with zero pressure is healthy, while one at 60% with rising memory pressure is already slowing down. Modern monitoring tools and Kubernetes increasingly use these metrics.

Namespaces and Cgroups Are Not the Whole Story

Namespaces and cgroups are the foundation of container isolation, but they are not a complete security boundary on their own. All containers share the host's kernel, so a kernel vulnerability can affect every container. Docker adds several more layers:

Layer

What it does

Linux capabilities

Splits root's power into about 40 separate privileges and grants containers only a small default set. Drop more with --cap-drop ALL.

Seccomp

Filters which system calls a container may make. Docker's default profile blocks dozens of dangerous calls.

AppArmor or SELinux

Mandatory access control profiles that restrict file and resource access even for root.

Masked and read-only paths

Sensitive parts of /proc and /sys are hidden or read-only inside containers.

no-new-privileges

Prevents processes from gaining privileges through setuid binaries.

User namespaces or rootless mode

Ensure root in the container is not root on the host.


Warning about --privileged: The --privileged flag disables most of these protections: it grants all capabilities, disables seccomp and AppArmor confinement and gives access to host devices. A privileged container is only lightly separated from the host. Avoid it unless you fully trust the workload.

For workloads that need stronger isolation, such as untrusted multi-tenant code, sandboxed runtimes such as gVisor (which intercepts system calls in a user-space kernel) or Kata Containers (which runs each container in a lightweight virtual machine) add a further boundary while keeping the container workflow.

Namespaces and Cgroups in Kubernetes

Kubernetes uses the same kernel features, organised around pods:

  • All containers in a pod share the network namespace (they reach each other on localhost) and the IPC namespace. Sharing the PID namespace is optional (shareProcessNamespace: true).

  • Pod resource requests and limits become cgroup settings: limits.memory sets memory.max, limits.cpu sets cpu.max, and requests.cpu sets cpu.weight.

  • Pods with hostNetwork, hostPID or hostIPC set to true share the node's namespaces, with the same risks as Docker's host options.

  • Kubernetes supports user namespaces for pods (hostUsers: false), applying the same root-inside, unprivileged-outside mapping.

Our tutorials Kubernetes Resource Quotas and LimitRanges and Kubernetes SecurityContext Explained build on these foundations.

Quick Reference: Commands Used in This Guide

Command

Purpose

docker inspect -f "{{.State.Pid}}" <container>

Host PID of a container's main process

ls -l /proc/<pid>/ns/

Namespaces a process belongs to

lsns

List all namespaces on the host

unshare --pid --fork --mount-proc ...

Run a command in new namespaces

nsenter -t <pid> -n <command>

Run a command inside a process's namespaces

stat -fc %T /sys/fs/cgroup/

Check for cgroup v2 (cgroup2fs)

docker info --format "{{.CgroupVersion}}"

Cgroup version used by Docker

cat /sys/fs/cgroup/memory.max

Memory limit (inside a container)

cat /sys/fs/cgroup/cpu.stat

CPU usage and throttling statistics

docker stats

Live resource usage read from cgroups


Frequently Asked Questions

What is the difference between namespaces and cgroups?

Namespaces control what a process can see: its process list, network, filesystem, hostname and so on. Cgroups control how much a process can use: CPU, memory, number of processes and I/O. Containers need both, because isolation without limits lets one container starve the rest, and limits without isolation let containers interfere with each other.

How many namespaces does Linux have?

Eight: mount, UTS, IPC, PID, network, user, cgroup and time. Docker creates new mount, UTS, IPC, PID, network and cgroup namespaces for each container by default; it does not use user or time namespaces by default.

Are containers as secure as virtual machines?

Not by default. Virtual machines each have their own kernel, while containers share the host kernel, so the attack surface is larger. Capabilities, seccomp, AppArmor or SELinux, user namespaces and rootless mode narrow the gap, and sandboxed runtimes such as gVisor or Kata Containers can close it further for untrusted workloads.

Does Docker still support cgroup v1?

Docker Engine 29 still runs on cgroup v1 hosts, but cgroup v1 support was deprecated in Docker Engine 29.0, with no removal date set yet. All current major Linux distributions default to cgroup v2, and new deployments should use it.

Why does my container show the host's total memory in free or top?

Tools such as free read /proc/meminfo, which is not namespaced and reports the host's memory. The real limit is in /sys/fs/cgroup/memory.max. Modern language runtimes (recent JVMs, Node.js and Go) read cgroup limits directly, but older software may size itself based on the host's memory and get OOM-killed.

What happens when a container reaches its CPU limit?

It is throttled, not killed. The kernel pauses the container's processes until the next scheduling period. This shows up as higher latency and growing nr_throttled values in cpu.stat, rather than as an error.

Conclusion and Next Steps

Every container you run is the combination of an ordinary Linux process, a set of namespaces that give it a private view of the system, and a cgroup that limits what it can consume. Once you understand these two features, a lot of Docker behaviour stops being mysterious: why the main process is PID 1, why two containers can use the same port, why free shows the wrong memory, and why a container exits with code 137.

Use /proc/<pid>/ns, nsenter and the files in /sys/fs/cgroup to look beneath Docker when debugging, always set explicit resource limits in production, and remember that namespaces and cgroups are the foundation of container security, not the whole building.

Continue the Docker course with these tutorials on BitCodeMatrix:

  • Docker Architecture Explained — how the client, daemon, containerd and runc use these kernel features

  • Container Isolation and Linux Namespaces — another walkthrough of namespace isolation

  • Rootless Docker: Setup and Limitations — user namespaces put into practice

  • Docker Security Fundamentals — capabilities, seccomp and the other layers on top