Linux Namespaces and cgroups: The Foundation of Containers
AI generated
$
/etc
Linux · Namespaces · cgroups · Container Internals
Linux Namespaces and cgroups: The Foundation of Containers
What Docker actually does under the hood

Docker containers are not magic and not lightweight virtual machines either, they are ordinary Linux processes that the kernel starts with a restricted view of processes, networking, and the filesystem through namespaces, while cgroups cap their resource usage. Understanding both mechanisms lets you inspect and reason about any running container directly from the host.

17 min. read PID · Net · Mount · User Namespace cgroup v2 · Docker · Kernel

1. What namespaces and cgroups actually are

The first time someone starts a container, it often feels like getting a kind of mini virtual machine: its own filesystem, its own IP address, its own process list. In reality, a container is nothing more than a completely ordinary Linux process that the host kernel starts with a restricted view of the world. A quick look with ps aux on the host shows every container process sitting plainly in the process list, carrying its own PID that looks entirely normal outside the container. There is no hypervisor, no second kernel, and no emulated hardware, just clever kernel bookkeeping.

Two separate kernel mechanisms create this illusion. Namespaces control what a process can see at all: which other processes, which network interfaces, which filesystem. cgroups (control groups) control instead how much of the genuinely shared resources, such as CPU, memory, and I/O, a process is allowed to consume. Docker, Podman, containerd, and Kubernetes all build on exactly the same two building blocks, just with different orchestration layered on top. Anyone who can operate unshare and the cgroup filesystems directly also understands what every high-level tool is doing behind the scenes, and can debug container problems without relying on the abstraction.

2. The namespace types at a glance

The Linux kernel currently supports eight namespace types, each isolating a different resource class. The PID namespace gives a process its own numbering scheme, so the first process inside it gets PID 1, regardless of its actual PID on the host. The network namespace isolates network interfaces, routing tables, and firewall rules. The mount namespace provides its own view of the directory tree, the UTS namespace isolates hostname and domain name, and the IPC namespace separates System V IPC objects and POSIX message queues.

Two more types complete the picture: the user namespace lets a process run as root inside the container while being mapped to an unprivileged user on the host, which is a central security win. The cgroup namespace, finally, hides the host's actual cgroup hierarchy from the container. The lsns tool lists every active namespace on the system, including its type, the number of associated processes, and the owner. This overview is the first step toward understanding what isolation a running container is actually using.


#!/usr/bin/env bash
# List all active namespaces on the host, grouped by type
lsns -t pid,net,mnt,uts,ipc,user

# Create a fully isolated set of namespaces for a new bash shell
unshare --pid --net --mount --uts --ipc --fork --mount-proc bash

# Inside the new shell: only the shell itself and its children are visible
ps aux
# PID   TTY      TIME     CMD
# 1     pts/2    00:00:00 bash
# 12    pts/2    00:00:00 ps

3. PID and network namespace in detail

The PID namespace is the most visible one: inside a freshly started container, ps aux usually shows only a handful of processes, led by PID 1. On the host, however, the same process carries a completely different, usually much higher PID. PID namespaces can also be nested: a process in the outer namespace sees every process in the inner namespaces, but not the other way around, an inner process can never see or signal an outer process. This one-way visibility is the core of the isolation.

The network namespace goes even further: a new network namespace starts with no network interfaces at all except its own, isolated loopback interface. Docker connects a container through a virtual Ethernet pair (veth) to a bridge on the host, so the container namespace sees one side of the pair as eth0 while the other side lands on the host bridge. With ip netns exec or directly through nsenter --net, any network namespace can be entered and examined individually, completely independent of whether Docker was involved at all.

4. Mount and user namespace: filesystem and privileges

The mount namespace is the foundation for a container's isolated root filesystem. Inside a new mount namespace, a process can switch to a completely different root directory with pivot_root or chroot, without affecting the host or other namespaces. Mount propagation matters here: by default, new mounts are private, meaning changes in the container's namespace stay invisible to the host and vice versa. If a mount is marked shared, changes propagate in both directions, which is used deliberately for bind mounts between host and container.

The user namespace additionally shifts the meaning of user and group IDs. A process can run as UID 0, root, inside its own namespace while still being mapped to UID 100000 or another unprivileged ID on the host. This mapping is configured through /proc/PID/uid_map and /proc/PID/gid_map. Without user namespace remapping, root inside the container is effectively root on the host the moment a kernel bug or a misconfiguration breaks the isolation, which is exactly why rootless Podman and Docker's userns-remap feature actively use this mechanism to reduce that risk.

5. cgroups: resource limiting instead of isolation

While namespaces decide what a process is allowed to see, cgroups decide how much of the shared system resources it may consume. Without cgroups, a single container could bring down the entire host through a memory leak, since it could theoretically claim all available memory. cgroups prevent that through hard or soft limits per controller: cpu for compute time, memory for RAM, io for block device throughput, and pids for the maximum number of processes, an effective safeguard against fork bombs inside a container.

cgroups are organized hierarchically and managed through a virtual filesystem under /sys/fs/cgroup, not through a classic system call API. In practice, that means setting a limit is essentially writing a text value into a specific file. A Docker container started with --memory=512m and --cpus=1.5 creates exactly such a cgroup behind the scenes, with the corresponding files set, which the kernel scheduler and memory controller respect on every resource allocation.

6. cgroup v2: hierarchy and controllers in practice

cgroup v2 replaced the older, often inconsistent v1 hierarchy with its several separate controller trees, with a single, unified hierarchy. All modern distributions running systemd as their init system use cgroup v2 by default, recognizable by a single mountpoint under /sys/fs/cgroup instead of the earlier split by controller. Every cgroup is represented as a directory, every resource limit as an individual file inside it, such as cpu.max for the CPU quota or memory.max for the hard memory ceiling.

Systemd integrates cgroup v2 deeply into its unit files: a service with MemoryMax=512M and CPUQuota=150% in its unit file automatically creates the matching cgroup structure under /sys/fs/cgroup/system.slice/. For container runtimes, that means both Docker and systemd access the same cgroup v2 hierarchy, which avoids conflicts and enables consistent monitoring through tools like systemd-cgtop, regardless of whether a process was started through Docker or directly through systemd.


# /etc/systemd/system/webapp.service
[Unit]
Description=Webapp with cgroup v2 resource limits
After=network.target

[Service]
ExecStart=/usr/bin/webapp --port 8080
# CPU quota: 150% of one core, written to cpu.max
CPUQuota=150%
# Hard memory ceiling, written to memory.max
MemoryMax=512M
# Soft memory floor, written to memory.low
MemoryLow=128M
# Fork bomb protection, written to pids.max
TasksMax=200
Restart=on-failure

[Install]
WantedBy=multi-user.target

7. Inspecting a running container process from the host

The practical payoff of this knowledge shows up when debugging: instead of relying exclusively on docker exec, any container process can be examined directly from the host. The first step is finding the main process's PID, either through docker inspect --format '{{.State.Pid}}' <container> or directly through ps aux | grep containerd-shim. With that PID, the process's namespace memberships can be read straight from /proc/PID/ns/: every file there is a symbolic link to a namespace, whose inode number uniquely identifies which processes share that same namespace.

Two processes with an identical inode number under /proc/PID/ns/net share exactly the same network namespace, regardless of whether they were started through Docker or not. The nsenter command uses exactly this information to switch into a running process's namespaces, entirely without the Docker client. docker inspect itself also returns the actually configured cgroup limits in its HostConfig block in JSON, so a container's memory and CPU limits can be verified even without docker stats.


{
  "State": {
    "Pid": 48213,
    "Status": "running"
  },
  "HostConfig": {
    "Memory": 536870912,
    "MemorySwap": 536870912,
    "NanoCpus": 1500000000,
    "PidsLimit": 200,
    "CgroupParent": ""
  },
  "NetworkSettings": {
    "SandboxKey": "/var/run/docker/netns/a1b2c3d4e5f6"
  }
}

#!/usr/bin/env bash
set -euo pipefail

# Find the main process PID of a running container
CONTAINER="webshop-app"
PID=$(docker inspect --format '{{.State.Pid}}' "$CONTAINER")
echo "Host PID: $PID"

# Every namespace the process belongs to is a symlink here
ls -la /proc/"$PID"/ns/
# net:[4026532451]  mnt:[4026532448]  pid:[4026532452] ...

# Enter the container's network and mount namespace directly from the host
sudo nsenter --target "$PID" --net --mount --pid bash

# Confirm: two processes sharing a namespace show the same inode number
readlink /proc/"$PID"/ns/net
readlink /proc/self/ns/net

8. Building your own sandboxes with unshare and nsenter

The unshare command creates new namespaces for a process, completely without a container runtime. Running unshare --pid --net --mount --uts --ipc --fork --mount-proc bash starts a new bash shell that runs under practically the same isolation as a freshly started Docker container, just without an overlay filesystem and image layers. The --mount-proc flag is essential: it ensures /proc gets remounted inside the new mount namespace, otherwise ps aux in the new shell would still show the host's processes, because /proc is itself a mounted filesystem that, without a fresh mount, keeps pointing at the old namespace.

The counterpart to unshare is nsenter: instead of creating new namespaces, it enters the namespaces of an already running process. nsenter --target <pid> --pid --net --mount bash opens a shell with exactly the same view as the target process, which in practice makes an excellent debugging tool for containers without a working shell in the image, such as minimal distroless images where docker exec fails for lack of a shell. Root privileges on the host are generally required for both commands, since entering another process's namespaces is a privileged operation.

9. Security boundaries: namespaces and cgroups compared

Namespaces and cgroups are powerful isolation tools, but they are not a security boundary on the level of a virtual machine. Every container on a host shares the same kernel: a kernel vulnerability, a bug in namespace code, or a misconfigured capability can theoretically escape the isolation. Tools like seccomp profiles, AppArmor, or SELinux therefore add extra layers on top of namespaces and cgroups, blocking individual system calls or further restricting file access, even when a process is formally running inside the correct namespace.

In practice, this frequently leads to wrong assumptions about what containers actually deliver. The table below contrasts common misconceptions with the correct way to think about them, each with a look at the concrete operational consequence.

Area Common misconception Correct understanding Why it matters
Process isolation chroot alone is enough Combine PID and mount namespace chroot is easily bypassed via directory traversal
Resource limiting ulimit per process is sufficient cgroup v2 with cpu.max and memory.max ulimit only applies per process, not per group
Security level Container equals VM-level isolation Namespaces share the host kernel A kernel exploit could theoretically affect every container
Network separation iptables rules alone are enough Own net namespace with veth and bridge Without its own namespace, processes share the same stack
Container debugging docker exec is the only access path nsenter directly via /proc/PID/ns Also works with minimal images that lack a shell

Anyone running especially sensitive workloads should think beyond plain namespace isolation. Runtimes like gVisor or Kata Containers add an extra abstraction layer, either through a user-space kernel or a lightweight VM per container, closing exactly the gap that pure namespace and cgroup isolation leaves open.

Mironsoft

Kernel-level Linux administration, container hardening, and deployment infrastructure

Container infrastructure you actually understand?

We audit your container and deployment environment, review namespace and cgroup configuration for security gaps, and build resilient, transparent infrastructure instead of black-box deployments.

Namespace & cgroup audit

Review isolation boundaries and resource limits of your containers

Container hardening

Set up seccomp, user namespace remapping, and read-only filesystems

CI/CD & deployment

Robust deployment pipelines with reproducible resource limits

10. Summary

Linux namespaces and cgroups solve two separate problems: namespaces determine what a process is allowed to see, cgroups determine how much of the shared resources it is allowed to consume. Together they form the foundation that Docker, Podman, containerd, and Kubernetes build on, without needing a hypervisor or a second kernel. PID, network, and mount namespace isolate the three resource classes users most commonly associate with containers, while the user namespace additionally decouples root privileges inside the container from real root privileges on the host.

The practical payoff of this knowledge lies in debugging: with /proc/PID/ns/, lsns, and nsenter, any running container process can be examined directly from the host, regardless of whether a shell exists inside the image or the Docker client happens to be working. What remains true is that namespaces and cgroups are not a security boundary at VM level, because every container shares the same kernel. seccomp, AppArmor, user namespace remapping, and, in extreme cases, runtimes like gVisor or Kata Containers add isolation exactly where it genuinely matters.

Linux Namespaces and cgroups, the key takeaways

Namespaces = visibility

PID, net, mount, UTS, IPC, user, and cgroup namespace determine what a process can see at all.

cgroups = resource limits

cpu.max, memory.max, pids.max cap how much of the shared resources a process uses.

Directly inspectable

/proc/PID/ns/, nsenter, and lsns work on any Linux host, independent of Docker.

No VM-level isolation

All containers share the host kernel. Use seccomp, AppArmor, and user namespace remapping as extra layers.

11. FAQ: Linux Namespaces and cgroups

1What is the difference between namespaces and cgroups?
Namespaces determine what a process can see. cgroups determine how much of shared resources it can consume. Together they form the foundation of containers.
2How many namespace types does the Linux kernel support?
Eight: PID, network, mount, UTS, IPC, user, cgroup, and time. Each isolates a different resource class.
3Why does ps aux inside a container show only a few processes?
The PID namespace gives the container its own process numbering. Inner processes cannot see outer processes.
4Is a container as secure as a virtual machine?
No, every container shares the host kernel. Extra layers like seccomp and AppArmor are needed for a real security boundary.
5How do I find the PID of a running Docker container?
With docker inspect --format '{{.State.Pid}}' , or via ps aux | grep containerd-shim.
6What does --mount-proc do in unshare?
Remounts /proc in the new mount namespace, otherwise ps aux would still show the processes of the old namespace.
7What is the difference between cgroup v1 and v2?
v1 uses several separate controller trees, v2 uses a single, unified hierarchy under one mountpoint.
8How do I debug a container without a shell in the image?
nsenter --target --pid --net --mount bash opens a shell with the same view as the target process, without docker exec.
9What does the user namespace do for security?
Maps root inside the container to an unprivileged UID on the host, even root inside the container gets no real privileges outside.
10Can I use namespaces without Docker?
Yes, unshare and nsenter are standalone Linux tools and work independently of any container runtime.