2026
Linux Namespaces and Cgroups: The Building Blocks of Containers
Containers are not a kernel feature. They are regular Linux processes that namespaces keep isolated and cgroups keep constrained. Here is how both mechanisms work and why understanding them makes container behaviour predictable.
Containers did not arrive out of nowhere. There is no single kernel feature called a container. What you call a container is a regular Linux process that the kernel has been told to keep isolated from everything else running on the machine. The mechanisms that make that work are namespaces and cgroups. They have been in the Linux kernel for years. Docker and Kubernetes just made them accessible enough that you could use them without knowing much about them.
Namespaces handle isolation. They control what a process can see: which other processes exist, which network interfaces are available, what the filesystem looks like from the inside. Cgroups handle resource accounting. They control what a process can use: how much CPU time it gets, how much memory it is allowed to consume before the kernel intervenes.
Understanding these two mechanisms does not make you a kernel developer. It does make container behaviour predictable rather than mysterious. When a container gets killed unexpectedly or cannot see a network interface it should be able to reach, the answer is usually in here somewhere.
Namespaces
A namespace wraps a global system resource in a layer of abstraction so that processes inside the namespace believe they have their own private copy of that resource. Changes to the resource inside one namespace are invisible to processes in other namespaces. Every process on a Linux system is always inside a set of namespaces. On a host with no containerisation happening, all processes share the same namespaces. When you start a container, the container runtime creates new namespaces and launches the container processes inside them.
Linux currently has eight namespace types. A process can be in a different namespace for each one independently. The kernel tracks namespace membership through file descriptors in /proc/<pid>/ns/. Two processes sharing the same inode number for a given namespace type are in the same namespace.
mntMountControls what filesystem mounts a process can see. Each namespace gets its own view of the filesystem tree. Two processes can have entirely different root directories and mount points without affecting each other.
pidPIDIsolates the process ID number space. Processes inside a new PID namespace start at PID 1. From outside the namespace you still see the real PIDs. The init process inside the namespace believes it owns the world.
netNetworkGives a process its own network stack. Separate interfaces, routing tables, firewall rules and port space. This is how two containers on the same host can both listen on port 8080 without conflict.
utsUTS (Hostname)Isolates the hostname and domain name. A container can have its own hostname independent of the host. Named after Unix Time-sharing System, which is where these fields originated.
ipcIPCIsolates inter-process communication resources: System V message queues, semaphores and shared memory segments. Prevents processes in different namespaces from accidentally communicating through shared memory.
userUserMaps user and group IDs between the namespace and the host. A process can appear to run as UID 0 inside the namespace while mapping to an unprivileged UID on the host. This is the basis for rootless containers.
cgroupCgroupHides the host's cgroup hierarchy. The process sees a separate cgroup root, so it cannot observe or interfere with cgroup settings applied to other parts of the system.
timeTimeAllows different processes to have different views of the system clock offsets. Added in Linux 5.6. Less commonly used than the others but useful for testing time-sensitive code in isolation.
How namespaces are created
Namespaces are created through three syscalls. clone() creates a new process in new namespaces in one step. unshare() moves the calling process into new namespaces without forking. setns() moves a process into an existing namespace by passing a file descriptor. Container runtimes use combinations of all three when setting up a container.
PID namespaces need a fork
When you create a new PID namespace, the calling process retains its original PID in the parent namespace. The first child forked into the new namespace becomes PID 1 inside it. This is why unshare --pid requires --forkto be useful. Without forking, no process inside the new namespace would have PID 1, which means the namespace would be effectively broken from the start.
lsnsList all namespaces visible to the current user. Shows the namespace type, inode number, number of processes inside it and the command that created it.
lsns -t netFilter to network namespaces only. Useful for seeing which processes are running with isolated network stacks.
unshare --net --pid --fork bashStart a new shell in fresh network and PID namespaces. The --fork flag is required when creating a new PID namespace because the calling process cannot become PID 1 itself.
nsenter -t <pid> --net --pidJoin the namespaces of an existing process. What docker exec uses under the hood when you open a shell inside a running container.
ip netns listList named network namespaces managed by iproute2. These are network namespaces that have been given a name and appear as files under /var/run/netns.
ip netns exec <name> ip addrRun a command inside a named network namespace. Good for inspecting or modifying the network stack of a specific namespace directly.
readlink /proc/<pid>/ns/netShow the inode number for the network namespace of a specific process. Two processes sharing the same inode are in the same namespace.
Cgroups
Control groups organise processes into a hierarchy and apply resource limits to each node in that hierarchy. Every process belongs to exactly one cgroup at each level. Limits set on a parent apply to everything under it. You can give a group of processes a combined memory limit without setting individual limits on each one. The kernel does the accounting for you.
The interface to cgroups is the filesystem. Everything under /sys/fs/cgroup/is a virtual filesystem that the kernel populates. You read resource usage by reading files. You set limits by writing to files. You create a new cgroup by creating a directory. Removing a cgroup means removing the directory after all its processes have moved elsewhere.
cpuSets the relative CPU weight and hard limits for a group. The scheduler uses weights to decide how much CPU time each group gets when the CPU is fully loaded.
memoryTracks memory usage and enforces limits. When a group exceeds its limit the kernel can either kill processes inside it or trigger the OOM killer on the group.
blkioControls the rate at which a group can read from and write to block devices. Useful for preventing a noisy container from saturating disk I/O for everything else on the host.
cpusetPins processes to specific CPU cores and NUMA nodes. Useful in latency-sensitive workloads where you want a process to stay on particular cores.
pidsLimits the number of processes that can exist inside a group. A container that forks uncontrollably hits this ceiling rather than consuming PIDs from the host.
net_cls / net_prioTags network packets from a group with a class ID that network rules can act on. Lets you apply traffic shaping per group.
Cgroup v1 vs cgroup v2
Cgroup v1 grew organically over the years and it shows. Each subsystem had its own independent hierarchy mounted separately. A process could be in one part of the CPU hierarchy but a completely different part of the memory hierarchy. In practice this created inconsistencies that were difficult to reason about.
Cgroup v2 replaced that with a single unified hierarchy. All subsystems share the same tree. A process is in exactly one location in the hierarchy and all resource limits apply at that location. Most modern distributions default to v2 now. Docker switched its default to v2 in recent versions. If you are reading documentation written before roughly 2021 that talks about cgroups, assume it describes v1 unless it says otherwise.
Working with cgroups
You rarely manipulate cgroups directly in production. Systemd manages them for services. Container runtimes manage them for containers. But knowing how to inspect the hierarchy and read what limits are in place is genuinely useful when you need to understand why a process got killed or why a service is being throttled.
cat /proc/self/cgroupShow which cgroup the current process belongs to. The output format changed between cgroup v1 and v2. In v2 you will see a single unified hierarchy.
systemd-cglsPrint the cgroup tree as systemd sees it. Shows services and slices arranged in a hierarchy, each owning the processes running under it.
systemd-cgtopLive view of cgroup resource usage. Shows CPU, memory and disk I/O per cgroup, updated in place. Think of it as top but organised by service rather than individual process.
cat /sys/fs/cgroup/memory/<group>/memory.limit_in_bytesRead the memory limit for a specific cgroup (v1 path). The value -1 means no limit is set.
cat /sys/fs/cgroup/<group>/memory.maxRead the memory limit for a cgroup using the v2 unified hierarchy. The string 'max' means no limit.
echo 512M > /sys/fs/cgroup/<group>/memory.maxSet a 512 MB memory limit on a cgroup in the v2 hierarchy. Any process in that group that tries to exceed this will trigger the out-of-memory handler.
How namespaces and cgroups work together
A container is a process with a full set of namespaces applied plus a cgroup to constrain its resource usage. The namespaces build the walls. The cgroup puts a ceiling on the room. Neither mechanism knows about the other at the kernel level. A process can have namespaces without cgroup limits, or cgroup limits without namespace isolation. Container runtimes use both together because isolation without resource limits leaves you vulnerable to noisy-neighbour problems and resource limits without isolation do not prevent processes from interfering with each other in other ways.
Namespaces give you
- Isolated filesystem view
- Private process tree
- Separate network stack
- Independent hostname
- Rootless operation via user ns
Cgroups give you
- CPU time limits
- Memory caps with OOM handling
- Disk I/O throttling
- Process count limits
- Per-group resource accounting
When the memory limit is hit
When a process in a cgroup exceeds its memory limit, the kernel runs the out-of-memory killer against processes inside that group. It picks a victim based on a score that accounts for how much memory the process is using relative to its size. The chosen process is sent SIGKILL. This is why containers sometimes die without warning. From inside the container it looks like the process simply disappeared. From the host you will see an OOM kill event in the kernel log.
The event shows up in dmesg as a line containing oom-kill: followed by the cgroup path, the process name, the PID killed and the amount of memory involved. This is the first place to look when a container exits unexpectedly with no error in its own logs.
oom-kill: constraint=CONSTRAINT_MEMCG, nodemask=(null), cpuset=mycontainer, mems_allowed=0, oom_memcg=/system.slice/docker-abc123.scope, task_memcg=/system.slice/docker-abc123.scope, task=node, pid=3141, uid=0
The cgroup path in oom_memcg tells you exactly which container was involved. The task name and PID identify what was killed inside it.
Where it fits
Every container you run is built on these two mechanisms. When Docker creates a container, it calls into the kernel to set up namespaces and create a cgroup. When Kubernetes schedules a pod, the kubelet on each node uses the container runtime to do the same thing and connects the cgroup to the resource limits set in the pod spec. The resources.limits.memory field in a Kubernetes manifest becomes a value written into a cgroup memory file on the node.
Systemd uses cgroups for the same reason outside of containers. Every service unit gets its own cgroup slice. Transient resources are cleaned up when the slice is removed. The slice hierarchy you see in systemd-cgls is the kernel cgroup hierarchy with a layer of naming on top.
Security tools like seccomp and AppArmor operate at the process level and complement namespace isolation without replacing it. Namespaces limit what a process can observe. Seccomp limits which syscalls it can make. Both work on the same process and the container runtime applies both when a security profile is configured. Knowing where the boundaries are makes debugging a permission failure much less guesswork.