How Containers Work: Linux Namespaces, Cgroups, Union Filesystems, and the Runtime Architecture From docker run to Running Process
A deep dive into container internals covering Linux namespaces for process isolation, cgroups v2 for resource limiting, OverlayFS and image layer copy-on-write semantics, the OCI runtime spec and containerd/runc execution flow, container networking with veth pairs and bridge devices, and a tradeoffs comparison against VMs, Firecracker, gVisor, and Wasm containers. With production guidance.
Most engineers think of containers as “lightweight VMs.” That framing is wrong, and it causes real operational mistakes. Containers are not virtual machines with a smaller footprint. They are a set of Linux kernel features composed together to create an isolated process environment, sharing the same kernel as the host.
This article walks through every layer of that composition: the six Linux namespaces that create isolation boundaries, cgroups v2 and how the kernel enforces resource limits, OverlayFS and the copy-on-write layer model that makes image storage efficient, the OCI runtime spec and the containerd-to-runc execution path that turns an image into a running process, and container networking from veth pairs to port mapping. The final section covers tradeoffs against alternative isolation technologies and production considerations every engineer running containers in production should understand.
Linux Namespaces: What Isolation Actually Means
A container is, at its core, a process (or group of processes) running inside a set of Linux namespaces. Namespaces wrap a global system resource and make it appear to processes inside the namespace that they have their own isolated instance of that resource. There are six namespace types relevant to containers.
PID Namespace
The PID namespace isolates the process ID number space. A process inside a PID namespace sees itself as PID 1. The host can see all container PIDs (with different numbers), but the container cannot see host PIDs. This is why sending SIGKILL to PID 1 inside a container terminates the container: the container’s init process is PID 1 in its own namespace.
Network Namespace
Each container gets its own network namespace: a separate network stack with its own interfaces, routing table, iptables rules, and port space. Port 8080 inside a container is completely independent from port 8080 on the host. When you publish a port (-p 8080:80), you are creating a NAT rule between the host network namespace and the container’s network namespace.
Mount Namespace
The mount namespace isolates the filesystem view. Each container gets its own mount table. When a container mounts a tmpfs or bind-mounts a volume, those mounts are invisible to the host and to other containers. This is the mechanism that makes every container appear to have a clean root filesystem.
UTS Namespace
UTS (Unix Time-Sharing) isolates the hostname and NIS domain name. This is why each container can have its own hostname without affecting the host system. It is the smallest namespace in terms of implementation complexity, but it matters for service discovery and logging.
IPC Namespace
The IPC namespace isolates System V IPC objects and POSIX message queues. Processes in different IPC namespaces cannot communicate through shared memory segments or semaphore sets unless explicitly configured. This prevents side-channel attacks through shared memory between container workloads.
User Namespace
User namespaces allow a process to have a different UID/GID mapping inside the namespace versus the host. A process can appear as root (UID 0) inside the container while mapping to an unprivileged UID on the host (e.g., UID 100000). This is the foundation of rootless containers and significantly reduces the blast radius of a container escape.
// Reading a container's namespace links from /proc
import { readlinkSync, readdirSync } from "fs";
function inspectNamespaces(pid: number): Record<string, string> {
const nsDir = `/proc/${pid}/ns`;
const namespaces: Record<string, string> = {};
for (const entry of readdirSync(nsDir)) {
try {
namespaces[entry] = readlinkSync(`${nsDir}/${entry}`);
} catch {
// skip if unreadable
}
}
return namespaces;
}
// Example output for a container process:
// {
// pid: "pid:[4026532345]",
// net: "net:[4026532348]",
// mnt: "mnt:[4026532346]",
// uts: "uts:[4026532347]",
// ipc: "ipc:[4026532349]",
// user: "user:[4026532350]",
// }
Two processes sharing the same namespace inode number share the same namespace. This is how containers in the same pod share a network namespace in Kubernetes.
Cgroups v2: Resource Limiting Without Guessing
Namespaces provide isolation, but they do not limit resource consumption. A container with no cgroup constraints can consume all available CPU, memory, and I/O on the host. Cgroups (control groups) solve this.
Cgroups v2 is the unified hierarchy introduced in kernel 4.5 and now the default on all major distributions. Unlike v1, which had per-subsystem hierarchies that could conflict, v2 uses a single hierarchy mounted at /sys/fs/cgroup.
CPU Limiting
The CPU controller in cgroups v2 exposes two key files: cpu.max and cpu.weight. cpu.max implements hard throttling: 500000 1000000 means 500ms of CPU time per 1 second period (50% of one core). cpu.weight sets relative shares for fair scheduling under contention.
When a container hits its CPU quota, the kernel throttles it: processes are placed in a throttled state and blocked until the next period. This creates latency spikes at percentile tails. A container configured to 0.5 CPU that experiences a CPU burst will see p99 latency jump in the instant it gets throttled, even if the host has idle capacity on other cores.
Memory Limiting
memory.max sets a hard memory limit. When a container exceeds this limit, the kernel’s OOM killer terminates processes inside the cgroup. memory.high sets a soft limit: the kernel applies memory pressure and reclaims aggressively before the hard limit is hit, allowing a brief burst without an OOM kill.
The memory.current file shows live consumption. The most important file for capacity planning is memory.stat, which breaks down RSS, cache, slab, and mapped file bytes separately. Container memory limits that don’t account for page cache can cause unexpected OOM kills when the process reads files into cache.
I/O Limiting
The io.max file limits read/write IOPS and bandwidth per block device:
259:0 rbps=104857600 wbps=52428800 riops=1000 wiops=500
The io.stat file shows actual usage per device. Production I/O limits are frequently misconfigured because engineers set them on the device used at container start time, but the container may use a different device (e.g., a bind-mounted volume on a different disk).
OverlayFS: Image Layers and Copy-on-Write
Container images are not single files. They are a stack of read-only layers. OverlayFS (overlay filesystem) is the Linux kernel filesystem that assembles these layers into a coherent directory tree.
Layer Architecture
┌─────────────────────────────────┐
│ Container Write Layer (rw) │ ← upperdir (per-container, ephemeral)
├─────────────────────────────────┤
│ Image Layer N (ro) │
├─────────────────────────────────┤
│ Image Layer N-1 (ro) │ ← lowerdir (shared across all containers
├─────────────────────────────────┤ using the same image)
│ Image Layer 1 (ro) │
├─────────────────────────────────┤
│ Base Layer (ro) │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ Merged View (container sees) │ ← merged
└─────────────────────────────────┘
OverlayFS mounts the read-only image layers as lowerdir (a colon-separated list, bottom to top) and the container’s writable layer as upperdir. The merged directory is what the container actually sees.
Copy-on-Write Semantics
When a container writes to a file that exists in a lower layer, OverlayFS copies the entire file into upperdir before applying the write. This is copy-on-write (CoW). The original in lowerdir is untouched, and other containers sharing the same image are unaffected.
Deleting a file in a container creates a “whiteout” file in upperdir: a character device with major/minor number 0/0 that masks the file in the lower layers.
The critical implication: writing many large files inside a container is expensive. Each write triggers a full copy from lower to upper before the write occurs. Writing a 1 GB database file inside the container layer creates 1 GB of data in upperdir. This is why databases should always use volumes (bind mounts or named volumes), not the container write layer.
Image Storage on Disk
Each image layer is stored as a directory under the container runtime’s storage root (typically /var/lib/docker/overlay2/ or /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/). Layers are content-addressed using their SHA256 digest. Two images sharing a base layer store that base layer once on disk.
The OCI Runtime Spec and the containerd/runc Execution Path
When you run docker run nginx, a precise chain of components executes.
Component Architecture
docker CLI
│
▼
dockerd (Docker daemon)
│ gRPC
▼
containerd
│ executes
▼
containerd-shim-runc-v2
│ forks
▼
runc (OCI runtime)
│ syscalls
▼
Linux kernel (namespaces + cgroups + seccomp)
│
▼
container process
The OCI Runtime Spec
The Open Container Initiative (OCI) Runtime Specification defines what a container runtime must do. It takes a filesystem bundle (a root filesystem directory plus a config.json file) and creates a running container.
config.json specifies namespaces to create, cgroup resource limits, capabilities to grant or drop, seccomp profile to apply, the process to run, environment variables, and mount points. The spec is implementation-agnostic: runc, crun, gVisor’s runsc, and Kata Containers’ kata-runtime all consume the same spec.
runc Execution
runc is a thin CLI that executes the OCI spec. When called by containerd, it:
- Clones the process with
clone(2)using namespace flags (CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC). - Sets up the mount namespace: pivots the root to the image’s OverlayFS mount point using
pivot_root(2). - Applies cgroup resource limits by writing to the cgroup v2 hierarchy.
- Drops Linux capabilities not in the allowlist (containers run without
CAP_SYS_ADMINby default). - Applies the seccomp filter to block dangerous syscalls.
- Executes the container entrypoint via
execve(2).
// Simplified representation of an OCI config.json
interface OCIConfig {
ociVersion: string;
process: {
terminal: boolean;
user: { uid: number; gid: number };
args: string[];
env: string[];
cwd: string;
capabilities: {
bounding: string[];
permitted: string[];
effective: string[];
};
rlimits: Array<{ type: string; hard: number; soft: number }>;
noNewPrivileges: boolean;
seccompProfile: string;
};
root: { path: string; readonly: boolean };
mounts: Array<{ destination: string; type: string; source: string; options: string[] }>;
linux: {
namespaces: Array<{ type: string }>;
resources: {
memory: { limit: number; swap: number };
cpu: { quota: number; period: number; shares: number };
};
cgroupsPath: string;
seccomp: object;
};
}
containerd-shim
The containerd-shim (containerd-shim-runc-v2) is a small process that persists between container lifecycle events. When containerd restarts, the shim keeps the container running. The shim handles stdio piping, exit code reporting, and re-attachment. Without the shim, stopping containerd would kill all running containers.
Container Networking: veth Pairs, Bridge, and Port Mapping
The default container network mode (bridge) creates a virtual network inside the host.
Setup Sequence
Host network namespace Container network namespace
┌──────────────────────┐ ┌──────────────────────────┐
│ docker0 (bridge) │ │ eth0 │
│ 172.17.0.1/16 │ │ 172.17.0.2/16 │
│ │ │ │ │ │
│ veth0 (host end) ◄──┼────────┼──► veth1 (container end) │
└──────────────────────┘ └──────────────────────────┘
│
│ iptables MASQUERADE (SNAT)
▼
host eth0
(internet)
When a container is created:
- The runtime creates a veth pair: two virtual Ethernet interfaces linked back-to-back. Packets sent into one end emerge from the other.
- One end (
veth0) is placed in the host namespace and attached to thedocker0bridge. - The other end is moved into the container’s network namespace and renamed
eth0. - The bridge assigns an IP from its subnet to the container’s
eth0. - An iptables
MASQUERADErule on the host NATs outbound traffic from the container subnet to the host’s external IP.
Port Publishing
Publishing a port (-p 8080:80) creates an iptables DNAT rule in the DOCKER chain:
-A DOCKER -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
Inbound TCP traffic on host port 8080 is destination-NAT’d to the container’s IP and port 80. The reply traffic is SNAT’d back through MASQUERADE. This chain is maintained by the container runtime automatically.
For Kubernetes environments, kube-proxy manages a similar set of iptables (or IPVS) rules at cluster scale, and CNI plugins replace the bridge model with more scalable overlay or BGP-based routing.
Tradeoffs: Containers vs. Alternative Isolation Technologies
| Technology | Kernel Sharing | Startup Time | Memory Overhead | Isolation Strength | Use Case |
|---|---|---|---|---|---|
| Linux containers (runc) | Shared host kernel | ~50-200ms | ~5-20 MB per container | Process-level (namespace + seccomp) | Standard microservices, CI workloads |
| Virtual machines (KVM/Xen) | Separate kernel per VM | ~5-30s | ~256 MB-1 GB per VM | Full hardware virtualization | Multi-tenant, compliance, hostile workloads |
| Firecracker microVMs | Separate kernel per microVM | ~125ms | ~5 MB per VM | VM-level isolation with minimal attack surface | Serverless (AWS Lambda), untrusted code |
| gVisor (runsc) | Host kernel intercepted via sentry | ~300-500ms | ~15-30 MB | Syscall interception (not full kernel exposure) | Untrusted containers, GKE Sandbox |
| Wasm containers (WASI) | No kernel sharing; runtime sandbox | ~1-5ms | ~1-5 MB | Capability-based sandbox | Edge functions, plugin systems, untrusted modules |
Containers give the best density and startup speed but expose the host kernel to all container processes. A kernel exploit in one container can compromise the host.
VMs give strong isolation but cost in memory, startup time, and density. Running 500 VMs on a single host is impractical; running 500 containers is routine.
Firecracker occupies the intersection: VM-level isolation (each microVM runs its own kernel) with startup times and memory overhead approaching containers. AWS Lambda and Fargate run on Firecracker. The tradeoff is that the guest kernel must be managed separately, and the device model is deliberately minimal (no USB, no PCI hotplug).
gVisor intercepts syscalls at a Go userspace layer (the “sentry”) before they reach the host kernel. This dramatically reduces the kernel attack surface. The cost is syscall interception overhead: applications that make many syscalls (Go runtimes, Java JVMs, database clients) can see 10-50% throughput reduction.
Wasm containers (via WASI and platforms like wasmtime or wasmedge) sandbox at the language level with capability-based security. They start in milliseconds and use minimal memory. The constraint is portability to Wasm bytecode, which requires either compiling from source or using a language with first-class Wasm support.
Production Considerations
CPU throttling causes latency spikes, not just slowness. When a container exceeds its CPU quota period, the kernel parks the process until the next period (typically 100ms). A container set to 500m CPU (50% of one core) can experience 100ms stalls even when the host has idle capacity. Setting cpu.max too conservatively creates p99 latency problems that look like application bugs. Profile actual CPU usage at peak before setting limits, and set cpu.high (soft limit) below cpu.max (hard limit) to give the kernel room to reclaim without throttling.
Container memory limits must account for page cache. The memory.max limit includes page cache. A process that reads 500 MB of files into cache while sitting at 400 MB RSS will OOM-kill if the limit is 800 MB, even though RSS alone fits. Either set limits with realistic cache headroom or configure memory.high to trigger earlier reclaim.
Ephemeral write layers accumulate fast. Every file written inside a container (logs, temp files, package downloads) goes into the OverlayFS upperdir. Over time, stopped containers leave their write layers on disk. docker system prune (or the containerd equivalent) must be part of the host maintenance cycle. Production hosts running CI workloads can exhaust disk space in hours without this.
PID namespace alone does not prevent privilege escalation. A container running as root with default capabilities can write to /proc/sysctl paths, load kernel modules if CAP_SYS_MODULE is present, or exploit kernel vulnerabilities. Always drop capabilities explicitly: at minimum, drop CAP_NET_RAW, CAP_SYS_PTRACE, and CAP_DAC_READ_SEARCH. Apply a seccomp profile. Run as a non-root UID. These three controls together reduce the kernel attack surface substantially.
User namespaces change the threat model meaningfully. With user namespace remapping, a container escape that gains UID 0 inside the container maps to UID 100000 on the host: an unprivileged user with no special capabilities. Enable userns-remap in containerd or --userns-remap in Docker for any workload where tenant isolation matters.
iptables rules accumulate at scale. On a host running hundreds of containers with port-published services, the iptables DOCKER chain can contain thousands of rules. iptables is O(n) per packet for rule evaluation. At this scale, switch to nftables or enable IPVS mode in kube-proxy, which uses hash-based lookup instead of linear rule traversal.
Image layer count affects startup time. OverlayFS mounts are faster with fewer layers. An image built from 50 intermediate layers produces a 50-entry lowerdir path. Squash build stages and use multi-stage builds to limit final layer count. Beyond about 128 layers, some kernels hit OverlayFS mount argument length limits.
Seccomp profiles are the most underused container security control. The default Docker seccomp profile blocks about 44 syscalls. It does not block ptrace, which allows one container process to inspect another. Custom seccomp profiles based on actual syscall usage (profiled with strace or sysdig) can reduce the exposed syscall surface to under 40 syscalls for a typical web service.
Containers are kernel features, not a layer above the kernel. Understanding namespace boundaries, cgroup enforcement mechanics, and OverlayFS CoW semantics gives you the diagnostic precision to explain the latency spikes, OOM kills, and disk exhaustion events that look mysterious from the outside. The runtime is thin; the kernel does the real work.
More in System Design
How Amazon Aurora Works Internally: The Log-Is-the-Database Architecture, Quorum Writes, and Storage-Compute Separation Behind Cloud-Native SQL
A deep dive into Aurora's storage-compute separation, the log-is-the-database design that pushes redo log processing to storage nodes, quorum-based replication across 6 copies in 3 AZs, protection group architecture, fast cloning, Serverless v2 scaling mechanics, and honest tradeoffs versus RDS, self-managed PostgreSQL, CockroachDB, and Cloud Spanner.
How CockroachDB Works Internally: Distributed SQL, Raft Consensus Per Range, and the Architecture Behind Serializable Transactions at Global Scale
A deep dive into CockroachDB's internals: the 512MB range-based data model with automatic splitting, per-range Raft replication, MVCC timestamp ordering with hybrid logical clocks, DistSQL physical planning, serializable transactions with timestamp refreshes, closed timestamps for follower reads, online schema changes using multi-version schema descriptors, and production considerations for hotspots and write amplification.
How Container Runtimes Work Internally: Namespaces, Cgroups, and the OCI Stack From Docker Run to Process Isolation
Containers are not virtual machines and they are not magic. They are a thin composition of Linux kernel primitives: namespaces, cgroups, and a layered filesystem. This article traces exactly what happens between docker run and a running process, covering the OCI spec, containerd, runc, overlay filesystems, and container networking at the kernel level.
How YugabyteDB Works Internally: DocDB Storage, Tablet Splitting, and the Dual API Architecture That Scales PostgreSQL Horizontally
A deep dive into YugabyteDB internals covering the DocDB storage engine built on RocksDB LSM trees, per-tablet Raft consensus, YSQL and YCQL dual query layers, distributed MVCC with hybrid logical clocks, automatic tablet splitting and rebalancing, xCluster multi-region replication, and production considerations for hotspots, connection pooling, and schema design.