What are cgroups and namespaces, and how do they make containers work?
Quick Answer
Namespaces give each container its own isolated view of resources (processes, network, filesystem, users). Cgroups limit how much CPU, memory, and I/O a container can use. Together, they are the kernel features that Docker, Podman, and Kubernetes all rely on.
Detailed Answer
Think of a large office building where namespaces are the walls and doors that create private offices. Each tenant sees only their own space and has their own reception desk (hostname), phone system (network), and file cabinets (filesystem). Cgroups are the building management system that controls how much electricity (CPU), water (memory), and elevator bandwidth (I/O) each tenant can use. Containers are simply these two things combined with a filesystem image, wrapped behind a friendly interface like Docker.
Linux namespaces create isolated views of system resources that would normally be shared. There are eight types: PID (each container gets its own process numbering starting from 1), Network (separate network stack with its own interfaces and routing), Mount (isolated filesystem mounts), UTS (separate hostname), IPC (isolated inter-process communication), User (maps container root to an unprivileged host user for rootless containers), Cgroup (isolated view of the cgroup tree), and Time (separate system clock, added in kernel 5.6). When Docker starts a container, it calls clone() with flags like CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS to put the new process in fresh namespaces. The unshare() call can move an existing process into new namespaces, and setns() lets a process join existing ones (this is how docker exec works).
Cgroups (control groups) v2 organize processes into a tree structure under /sys/fs/cgroup/. Each node in the tree has controller files that set resource limits. The memory controller (memory.max, memory.high) caps how much memory a group can use and triggers a cgroup-level OOM killer if the limit is exceeded. The CPU controller (cpu.max) throttles CPU time. For example, setting '100000 100000' gives the group 100% of one CPU core. The I/O controller (io.max) limits disk read/write speed and operations per second per device. The PID controller (pids.max) prevents fork bombs inside a container. When a process goes over its cgroup memory limit, the kernel kills a process inside that cgroup, not a random one system-wide like the global OOM killer would. Cgroups v2 (unified hierarchy) replaced v1 (separate hierarchies per controller) and is the default on modern distributions.
In production, knowing these building blocks is essential for debugging container resource issues. When a Kubernetes pod is OOMKilled, it is the cgroup memory controller firing, not the system OOM killer. The limits in your pod spec (like resources.limits.memory: 512Mi) translate directly to memory.max in the container's cgroup. CPU limits map to cpu.max with CFS throttling, and throttling metrics show up in cpu.stat (nr_throttled, throttled_usec). CPU requests map to cpu.weight (v2) or cpu.shares (v1), which provide proportional sharing rather than hard caps. Tools like systemd-cgtop show live cgroup resource usage, and looking directly at /sys/fs/cgroup/ reveals the actual kernel-level constraints.
A big gotcha is that many applications inside containers read /proc/meminfo and /proc/cpuinfo, which show the host's total resources, not the container's limits. This causes JVMs, Node.js, and other runtimes to auto-tune based on host memory, leading to OOM kills when they allocate more than the cgroup allows. Fixes include using tools like LXCFS to provide container-aware /proc files, or setting explicit runtime flags (like Java's -XX:MaxRAMPercentage). Another pitfall is cgroup v1 versus v2: Docker on older kernels uses v1 with separate hierarchies, while newer systems use v2's unified hierarchy, requiring different file paths when debugging. Check which version is active with stat -fc %T /sys/fs/cgroup/ (cgroup2fs means v2).