All notes

Namespaces and cgroups: the machinery beneath containers

Linux namespaces give container processes isolated views of the system. Control groups group and account for tasks, allowing configured controllers to prioritise, protect or limit their use of shared resources.

about 25 minutes min read

After learning that a container is still a Linux process, I was left with a practical question:

What makes that process feel as though it is running inside a separate machine?

Two pieces of Linux machinery provide much of the answer:

  • Namespaces change what a process can see.
  • Control groups, usually called cgroups, group and account for tasks so the kernel can prioritise, protect or limit their use of shared resources.

The short version is still the most useful:

Namespaces isolate views. Cgroups govern consumption.

Docker combines both around a process. The result can look like an isolated computing environment:

CONTAINER PROCESS
│
├── namespaced process tree
├── namespaced filesystem mounts
├── namespaced network stack
├── namespaced hostname
│
└── governed CPU, memory, process and I/O use

But the two mechanisms should not be blended into one vague idea called isolation.

A process can have a private-looking process tree and still exhaust host memory. It can have strict memory limits while sharing the host network namespace.

Question Namespace Cgroup
Primary purpose Change a task’s view of selected kernel resources Group and account for tasks, then apply resource policy
Typical examples PIDs, mounts, network, UTS, IPC, users CPU, memory, PIDs and I/O
Creates a private kernel? No No
Implies a hard resource limit? No Only when a controller and policy impose one
Complete security boundary? No No

Both are kernel policy around shared reality. Neither creates a miniature private machine.

1. Two mechanisms beneath the container boundary

A Linux process normally observes system resources through the kernel. It can inspect process IDs, mounted filesystems, network interfaces, hostnames, inter-process communication objects, user identities and resource state.

Namespaces allow the kernel to present different views of selected resources to different tasks.

HOST PROCESS

sees host process tree
sees host mounts
sees host network interfaces


CONTAINER PROCESS

sees container process tree
sees container mounts
sees container network interfaces

The underlying machine is still shared. The process receives a narrower perspective.

A cgroup solves another problem. It organises tasks into a hierarchy so the kernel can:

  • account for their use;
  • assign relative priority;
  • expose pressure and throttling;
  • enforce configured ceilings;
  • inherit policy from parent groups.

Merely placing a process in a cgroup does not guarantee that a hard CPU or memory limit exists. Docker containers have no automatic CPU or memory ceiling unless explicit settings or a broader parent policy provide one.

Namespace membership and cgroup membership belong to running tasks, not images.

IMAGE
  │
  ├──► CONTAINER A PROCESS
  │       namespaces A
  │       cgroup A
  │
  └──► CONTAINER B PROCESS
          namespaces B
          cgroup B

Both containers may use identical binaries while seeing different process trees and networks, and while receiving different resource policies.

This reinforces the distinction between packaging and runtime control:

Concern Mechanism
Which user-space files travel with the application? Image
Which system view does the running task receive? Namespaces
How is resource use grouped and governed? Cgroups
Which privileged operations remain available? Capabilities and security policy
Which host paths and devices cross the boundary? Mount and device configuration

A container boundary is assembled from several independent mechanisms.

2. Namespaces edit the process’s view

Linux supports several namespace types. Runtimes normally create a coordinated set, but each boundary can be shared or configured independently.

PID namespaces create process-ID hierarchies

A PID namespace gives tasks their own process-numbering hierarchy.

Suppose a container runs one application:

CONTAINER VIEW

PID 1
catalog-service


HOST VIEW

PID 24180
catalog-service

The container process has not acquired two independent existences. It has different identifiers in nested process views.

PID namespaces are hierarchical. A task is visible in its own PID namespace and in ancestor PID namespaces. A task in a child namespace cannot normally see unrelated tasks that exist only in an ancestor namespace.

HOST PID NAMESPACE
│
├── systemd
├── sshd
├── catalog-service
└── basket-service


CATALOG PID NAMESPACE
│
└── catalog-service

This limits process discovery and signalling from inside the child namespace. The host retains the broader view.

PID 1 has special behaviour

The first task in a PID namespace becomes PID 1 for that namespace. It is the namespace’s init process.

PID 1 must reap orphaned descendants, and Linux gives it special signal semantics. Signals from other members of the namespace are handled differently from signals sent to ordinary processes. If the namespace’s init process exits, the kernel terminates the remaining tasks in that PID namespace.

A shell wrapper such as:

#!/usr/bin/env bash

catalog-service

can leave the shell as PID 1, with the application as a child. Signals may stop at the shell, and terminated children may not be reaped correctly.

A stronger launcher often uses:

#!/usr/bin/env bash

exec catalog-service

exec replaces the shell with the service. The service becomes PID 1 and receives signals directly.

Another option is a small init process that forwards signals and reaps children.

The namespace isolates a process hierarchy. It does not teach the first process how to manage that hierarchy correctly.

Process tools depend on /proc

Commands such as ps and top normally learn about processes through /proc.

Creating a PID namespace alone does not automatically make process tools display the intended namespaced view. Container runtimes normally pair the PID namespace with a mount namespace containing a /proc filesystem mounted for that PID namespace.

Conceptually:

PID NAMESPACE

defines process identity and visibility


/PROC MOUNT

exposes that view to process-inspection tools

This is why tools such as unshare --mount-proc create a mount arrangement alongside the new PID namespace.

Mount namespaces define the filesystem map

A mount namespace gives a task its own view of mounted filesystems.

Inside a container, / may appear as:

/
├── app
├── bin
├── etc
├── lib
└── usr

The host has a different root view.

The storage driver and runtime first construct or expose a root filesystem from image content and writable runtime storage. The mount namespace then determines how that filesystem, volumes, bind mounts, temporary filesystems and virtual filesystems appear to the process.

This distinction matters:

  • image layering and copy-on-write behaviour come from the storage implementation;
  • mount namespaces control the visible mount topology.

A bind mount deliberately adds a host path to that topology:

docker run --rm \
  --mount type=bind,source="$PWD",target=/workspace \
  alpine \
  ls /workspace

The process now sees the selected host directory at /workspace.

The namespace controls the filesystem map. It does not guarantee that the map contains no roads back to the host.

Network namespaces provide separate network stacks

A network namespace can contain its own:

  • interfaces;
  • IP addresses;
  • routes;
  • port space;
  • loopback interface;
  • firewall and other network-related kernel state.

Two containers can both listen on port 8080 because their port spaces belong to different network namespaces.

CONTAINER A

127.0.0.1:8080


CONTAINER B

127.0.0.1:8080

A runtime can connect a namespace to the host using a virtual Ethernet pair and bridge:

CONTAINER
   eth0
     │
     ▼
VIRTUAL ETHERNET PAIR
     │
     ▼
HOST BRIDGE
     │
     ▼
HOST NETWORK

The namespace provides a private-looking network stack. Docker or another platform decides how that stack connects to the wider world.

Loopback belongs to the network namespace

Inside a container, 127.0.0.1 refers to that network namespace’s loopback interface.

It does not refer to:

  • the Docker host;
  • another container;
  • a database container;
  • a sibling service.

If catalog listens only on:

127.0.0.1:50051

only tasks sharing the same network namespace can reach it.

A caller in another container normally connects through an address on the container interface, and the server listens on an address such as:

0.0.0.0:50051

The namespace makes localhost genuinely local to that network view.

UTS, IPC and user namespaces narrow other views

A UTS namespace isolates host and domain names. A container can report bfstore-catalog while the host reports cloud-sandbox-node.

An IPC namespace isolates selected System V and POSIX message-queue resources. It does not prevent communication through paths deliberately shared through networks, files or mounted Unix sockets.

A user namespace maps user and group IDs between namespace views:

CONTAINER VIEW

UID 0


HOST VIEW

UID 100000

Container root may be privileged with respect to resources owned by its user namespace without being host root. The scope of a capability depends on which user namespace owns the affected resource. Some operations still require authority in the initial host user namespace.

This improves isolation, but it also complicates file ownership and permission interpretation across the mapping boundary.

Other namespaces follow the same pattern

Linux also provides namespaces for cgroup-hierarchy visibility and time-related state.

A cgroup namespace changes how cgroup paths appear to tasks. It does not create a separate resource-controller hierarchy.

A time namespace can offset monotonic and boot-time clocks, primarily supporting checkpoint, restore and migration use cases. It does not provide an independent wall clock, because it does not virtualise CLOCK_REALTIME.

The important lesson is not memorising a catalogue. It is recognising the pattern:

ONE SHARED KERNEL RESOURCE

presented through

DIFFERENT TASK-SPECIFIC VIEWS

Containers normally receive a conventional set of namespaces. Other designs can share selected ones deliberately.

Kubernetes Pods, for example, normally share one network namespace across their containers while retaining separate process and filesystem arrangements according to configuration.

3. Cgroups organise shared resource use

Namespaces can make the process’s world look small. They do not make the machine’s physical capacity private.

A task with its own PID, mount and network namespaces can still consume real host memory and CPU.

PRIVATE VIEW

does not imply

PRIVATE CAPACITY

A cgroup groups tasks into a kernel-managed hierarchy.

HOST
│
├── system processes
│
├── catalog cgroup
│   └── catalog-service
│
└── basket cgroup
    └── basket-service

The tasks still use host resources. The cgroup establishes the accounting and policy boundary.

Your world appears smaller. Your appetite has a budget.

Cgroups are hierarchical

Cgroups form a tree.

/
├── system.slice
├── user.slice
└── containers
    ├── catalog
    ├── basket
    └── inventory

A parent can represent a wider workload, tenant or service group. Children can represent containers or worker classes.

Ancestor policy matters. A child normally cannot escape constraints imposed by its parents.

This means an application can be limited even when its own container configuration does not show an obvious hard ceiling. A system service, Kubernetes Pod, tenant or node-level parent may impose the effective constraint.

Cgroup v1 and cgroup v2 organise controllers differently

Linux has two major cgroup interfaces.

In cgroup v1, controllers could be mounted in separate hierarchies. CPU, memory and PIDs could therefore organise tasks differently.

In cgroup v2, controllers use one unified hierarchy with a more consistent interface.

CGROUP V1

CPU hierarchy
memory hierarchy
PIDs hierarchy


CGROUP V2

one unified hierarchy
with enabled controllers

Modern Linux environments increasingly use cgroup v2.

For application developers, the main model remains:

  • tasks are grouped;
  • resource use is accounted for;
  • controllers apply policy to the group.

The exact files, units and delegation rules vary between cgroup versions and platform configurations.

The kernel’s unit is a task

Introductory explanations often say that cgroups contain processes. That is usually the right operational model for containers, where all threads of one process normally stay together.

At the kernel level, the unit is more precisely a task. Cgroup v2 also supports threaded subtrees for selected controllers, including CPU, cpuset and PIDs.

This matters when reading task counts and when reasoning about multithreaded runtimes.

A container may have one visible application process while consuming many kernel tasks.

4. Controllers govern different resources differently

Cgroup controllers do not all enforce policy in the same way.

Memory may lead to reclaim and OOM handling. CPU quota leads to throttling. A PIDs limit causes task creation to fail. I/O controls depend on the storage path and scheduler.

Calling all of these settings a “resource limit” can hide important differences.

Memory policy includes reclaim, swap and OOM behaviour

Docker can place a container under a memory limit:

docker run --rm \
  --memory 256m \
  bfstore/catalog-service

The kernel accounts memory use to the cgroup. If usage reaches a hard boundary and reclaim cannot free enough memory, one or more tasks may be killed.

MEMORY USE GROWS
      │
      ▼
CGROUP BOUNDARY REACHED
      │
      ▼
RECLAIM INSUFFICIENT
      │
      ▼
OOM POLICY SELECTS A VICTIM

On cgroup v2, useful controls and observations include:

  • memory.current, current usage;
  • memory.high, a pressure and throttling boundary;
  • memory.max, the hard maximum;
  • memory.swap.max, the swap ceiling;
  • memory.events, recorded pressure and OOM events;
  • memory.oom.group, whether the workload should be treated as a unit during OOM handling.

A memory setting is therefore only one part of the failure policy. Swap availability, reclaim behaviour and OOM grouping also shape what happens.

Docker’s --memory and --memory-swap settings interact. When swap is available and --memory-swap is not explicitly configured, the workload may be able to use additional swap beyond its memory limit, according to Docker and host policy.

The practical lesson is not to infer the complete memory model from one number.

A limit is also not a healthy operating target. A service with a 256 MiB boundary should not normally sit at 255 MiB while hoping reclaim remains kind.

Monitoring should distinguish:

  • working set;
  • temporary spikes;
  • pressure;
  • swap use;
  • limit proximity;
  • OOM events.

Container-scoped failure can protect the host, but the application still needs to recover from termination.

CPU weight, quota and cpusets answer different questions

Docker can configure a CPU quota using:

docker run --rm \
  --cpus 0.5 \
  bfstore/catalog-service

This normally limits CPU time over a scheduling period. It does not allocate half of a physical processor exclusively.

When the group exhausts its quota, the tasks are throttled until the next period.

CPU QUOTA EXHAUSTED
      │
      ▼
TASKS THROTTLED
      │
      ▼
LATENCY INCREASES

The application can remain alive while becoming too slow to complete useful work.

Three related controls answer different questions:

CPU mechanism Question
Weight or shares During contention, which group should receive more CPU time?
Quota How much CPU time may this group consume over a period?
Cpuset On which CPUs or NUMA nodes may its tasks run?

A weight is relative and matters when groups compete. A quota imposes a ceiling. A cpuset constrains placement.

Performance analysis should therefore inspect CPU use, throttling and placement, not only whether the process is running.

PIDs limits count tasks, including threads

Docker can apply:

docker run --rm \
  --pids-limit 100 \
  my-image

The PIDs controller limits kernel tasks, including threads as well as conventional child processes.

A Go or Java service can consume several tasks while appearing as one application process in a simple process listing.

When the limit is reached, further task creation fails.

The limit must account for:

  • runtime threads;
  • worker processes;
  • shell commands;
  • helper tools;
  • temporary spikes;
  • diagnostic activity.

The correct limit follows the task model, not merely the number of top-level services.

I/O policy depends on the complete storage path

Cgroups can account for and constrain block I/O.

This can prevent one workload from dominating a shared device, but the observed effect depends on:

  • cgroup version;
  • I/O scheduler;
  • block device;
  • filesystem;
  • storage driver;
  • virtualisation;
  • cloud-volume implementation.
CGROUP POLICY
      │
      ▼
FILESYSTEM AND BLOCK LAYERS
      │
      ▼
VIRTUAL OR PHYSICAL STORAGE

The cgroup expresses policy. The storage stack determines how effectively that policy becomes observable performance.

Accounting remains useful without a hard limit

A cgroup can report CPU, memory, PIDs and I/O information even when no strict ceiling is configured.

That accounting supports tools such as:

docker stats

It allows a platform to attribute resource use to a workload boundary rather than treating every host PID as unrelated noise.

Cgroups do not, however, provide visibility isolation.

Two tasks can share the host PID namespace while belonging to separate cgroups, or share one cgroup while living in separate network namespaces.

The mechanisms remain independent.

5. Docker assembles and can weaken the boundary

When I run:

docker run --rm \
  --memory 256m \
  --cpus 0.5 \
  --pids-limit 100 \
  bfstore/catalog-service

Docker does not teach the application to use fewer resources.

It turns runtime configuration into Linux process configuration.

DOCKER OPTIONS
      │
      ▼
OCI RUNTIME CONFIGURATION
      │
      ├── namespace membership
      ├── cgroup placement
      ├── filesystem mounts
      └── process configuration
              │
              ▼
          LINUX KERNEL

Docker is the control surface. Namespaces and cgroups are part of the machinery underneath.

Namespace sharing changes the view

A container can deliberately use the host network namespace:

docker run --rm \
  --network host \
  my-image

It can also share the host PID namespace:

docker run --rm \
  --pid host \
  my-image

These options may be useful for monitoring, debugging or infrastructure workloads.

They also remove ordinary namespace boundaries.

The phrase “inside a container” says little unless the namespace configuration is known.

Resource limits can be absent

A container with no explicit memory limit may consume memory according to host availability and ancestor cgroup policy.

A container with no CPU quota may compete freely within its parent group’s rules.

CONTAINER EXISTS

does not imply

HARD RESOURCE LIMIT EXISTS

The view can look bounded while consumption remains effectively unbounded.

Namespaces and cgroups are not the whole security wall

A secure container also depends on:

  • Unix permissions;
  • user-ID mappings;
  • Linux capabilities;
  • seccomp;
  • AppArmor or SELinux;
  • read-only filesystems;
  • restricted mounts;
  • device access;
  • protected daemon sockets;
  • network policy;
  • patched kernels.

A process can have a private mount namespace while receiving the host root filesystem through a bind mount. Its view is namespaced, but the content exposed through that view is catastrophically broad.

A process can have strict memory limits while retaining excessive capabilities. Its appetite is governed, but its authority is not.

Namespaces and cgroups are foundational. They are not the whole wall.

6. Inspecting the machinery

The container boundary becomes easier to trust when it can be inspected as ordinary Linux state.

Inspect namespace membership

Linux exposes namespace membership through /proc.

For a process with host PID 24180:

ls -l /proc/24180/ns

may show:

cgroup
ipc
mnt
net
pid
pid_for_children
time
time_for_children
user
uts

Each link identifies a namespace object. Tasks with matching namespace identifiers share that namespace.

The lsns command provides another view:

sudo lsns

Enter another task’s namespaces

After finding a container’s host PID:

docker inspect \
  --format '' \
  bfstore-catalog

the host can inspect its network namespace:

sudo nsenter \
  --target <PID> \
  --net \
  ip addr

or its mount namespace:

sudo nsenter \
  --target <PID> \
  --mount \
  findmnt

This is powerful debugging machinery. It also demonstrates the underlying model:

The container network view is a Linux network namespace associated with tasks.

Inspect cgroup membership and controller state

A task’s cgroup membership is visible through:

cat /proc/<PID>/cgroup

On a cgroup v2 host, the path belongs to the unified hierarchy.

Files in that cgroup may include:

memory.current
memory.max
memory.events
cpu.stat
cpu.max
cpu.weight
pids.current
pids.max
io.stat

The exact path depends on:

  • the init system;
  • container runtime;
  • cgroup driver;
  • service manager;
  • Pod or container identifiers.

Docker provides a friendlier summary:

docker stats bfstore-catalog

Reading the kernel files reveals where those numbers and events originate.

7. Pods, bfstore and the mental model

Kubernetes adds fleet-level scheduling and reconciliation, but the node still uses namespaces and cgroups to realise workload policy.

Containers in one Pod normally share one network namespace:

POD NETWORK NAMESPACE
│
├── application container
└── sidecar container

They receive one Pod IP, one port space and one loopback interface. They can retain separate image filesystems and process trees according to Pod settings.

Kubernetes resource requests and limits also need careful wording.

Requests help the scheduler plan placement and influence runtime resource policy. Limits establish enforceable boundaries where the operating system and controller support them.

CPU and memory do not behave identically:

Kubernetes setting Main role
CPU request Scheduling input and relative CPU weighting
Memory request Scheduling input and workload classification
CPU limit CPU quota and throttling
Memory limit Reactive memory enforcement and possible OOM termination
PIDs or other node policy Platform-specific task and resource governance

A memory request is not a simple preallocated block of physical RAM. A CPU limit is not a dedicated processor.

The kubelet and runtime translate Kubernetes policy into cgroup configuration on one Linux node. The kernel does not schedule containers across a fleet.

A bfstore example

Suppose the catalog service runs with:

docker run --rm \
  --name bfstore-catalog \
  --network bfstore \
  --memory 256m \
  --cpus 0.5 \
  --pids-limit 100 \
  bfstore/catalog-service:dev

Namespaces give it:

View Result
PID catalog-service appears as PID 1
Mount The image and configured mounts appear as /
Network The service receives its own interfaces, routes and port space
UTS The process receives its own hostname
User IDs may be mapped according to runtime configuration

Cgroups give it:

Resource Policy
Memory A 256 MiB memory boundary, with failure behaviour also affected by swap and OOM policy
CPU Up to half a CPU’s quota over the configured period
PIDs Up to 100 kernel tasks, including threads

The host still supplies the kernel, physical memory, CPUs, network implementation and storage devices.

Other controls determine which user runs the process, which capabilities it keeps, which system calls it may make, which paths are mounted and which services it can reach.

The container is not one isolation mechanism. It is a bundle of boundaries.

Machinery checklist

Concern Question
PID view Which tasks can the workload see and signal?
Init Which process reaps children and receives termination signals?
Process reporting Is /proc mounted for the intended PID namespace?
Filesystem Which mount tree does the process receive?
Host exposure Which bind mounts, sockets or devices cross the boundary?
Network Which interfaces, routes, ports and loopback does it use?
Identity How do namespace UIDs and GIDs map to host identities?
Memory Which maximum, swap, pressure and OOM policies apply?
CPU Are weight, quota or cpuset controls configured?
PIDs How many tasks, including threads, may be created?
I/O Can one workload monopolise shared storage?
Hierarchy Which ancestor cgroups also constrain the workload?
Missing limits Which resources remain effectively unbounded?
Security Which capabilities, system calls and paths remain accessible?
Inspection Can operators trace the runtime settings to kernel state?

The mental model I am keeping

My earlier model was:

CONTAINER

one box that isolates an application

The stronger model is:

                         LINUX TASKS
                               │
              ┌────────────────┴────────────────┐
              │                                 │
              ▼                                 ▼
         NAMESPACES                         CGROUPS
              │                                 │
              ▼                                 ▼
      isolated system views             grouped resource policy
              │                                 │
      ┌───────┼────────┐              ┌─────────┼─────────┐
      │       │        │              │         │         │
      ▼       ▼        ▼              ▼         ▼         ▼
     PID    mounts   network          CPU      memory     PIDs

Namespaces answer:

Which version of the system
does this task observe?

Cgroups answer:

How is this task grouped,
accounted for and governed
on the shared machine?

Neither creates a private kernel.

Neither packages the application.

Neither defines every security permission.

Docker combines them with image filesystems, mounts, capabilities, system-call filtering, security policy and virtual networking.

The machinery is less magical than the finished box suggests.

A container process still stands on the same Linux floorboards as every other process.

Namespaces place screens around its view.

Cgroups mark out how much of the room it may occupy.

References and further reading

Linux namespaces manual
Introduces Linux namespace types and the system calls used to create and manage isolated process views.

PID namespaces
Documents process-ID isolation, namespace hierarchy and PID 1 behaviour.

Mount namespaces
Explains independent mount views and mount-propagation relationships.

Network namespaces
Documents isolated network devices, protocol stacks, routing tables and port spaces.

User namespaces
Documents user and group ID mappings and capability scope across user namespaces.

Cgroup namespaces
Explains namespaced views of cgroup paths.

Time namespaces
Documents offsets for monotonic and boot-time clocks.

unshare
Documents creating namespaces and mounting /proc for a new PID namespace.

nsenter
Documents running a program inside another task’s namespaces.

lsns
Documents listing namespaces visible to the system.

Linux control groups v2
Documents the unified cgroup hierarchy, resource controllers, memory policy and threaded subtrees.

Docker resource constraints
Explains Docker configuration for memory, swap, CPU and related resource controls.

Docker container statistics
Documents runtime metrics and clarifies that the PIDS figure includes threads.

Kubernetes resource management
Explains requests, limits and the differing runtime behaviour of CPU and memory controls.

OCI runtime specification
Defines runtime configuration for namespaces, cgroups, mounts, capabilities and container processes.