Docker Demystified - Part 2: Linux Primitives — Namespaces, cgroups, and chroot

The Secret Behind Containers: They’re Not What You Think

Ever wondered how containers achieve isolation without being actual virtual machines? The answer lies in three Linux kernel primitives: namespaces, cgroups, and chroot. These features have existed since the early 2000s—Docker just made them accessible.

Let me show you the actual mechanics of what happens when you run docker run nginx.


The Illusion of Isolation: How Namespaces Trick Your Containers

Let me run a quick experiment on my Mac Mini. I’ll start an Nginx container and look at it from two perspectives:

# On my Mac, I run:
docker run -d --name web nginx

# Inside the container, check the processes:
docker exec web ps aux

What the container sees:

PID   USER     COMMAND
1     root     nginx: master process
7     nginx    nginx: worker process

The container thinks it’s the only thing running. PID 1 (the init process) is nginx. Clean and simple.

But here’s what’s actually happening on my Mac’s hidden Linux VM:

# Shell into Docker Desktop's Linux VM
docker run -it --privileged --pid=host alpine nsenter -t 1 -m -u -n -i sh

# Now look at the real processes:
ps aux | grep nginx

What the host sees:

PID     USER     COMMAND
48291   root     nginx: master process
48332   nginx    nginx: worker process

That container’s “PID 1” is actually PID 48291! The Linux kernel is lying to the container, giving it a fake view of the world. That’s namespaces in action.

The Eight Types of Namespaces

Linux provides eight different types of isolation, each added over the years as containers evolved:

NamespaceYear AddedWhat It Fakes
pid2002Process IDs - Makes your container think it’s PID 1
net2009Network stack - Own IP address, ports, routing tables
mnt2002Filesystem mounts - Gets its own root / directory
uts2006Hostname - Container can be “webserver-1” while host is “mac-mini”
ipc2006Inter-process communication - Isolated shared memory
user2012User IDs - “root” in container = unprivileged user on host
cgroup2016Resource visibility - Container sees only its own limits
time2020System clock - Container can even have different time!

Here’s the mind-bending part: A container running as “root” inside might actually be running as user ID 100000 on the host. The kernel remaps everything. This is why rootless containers (like Podman) are possible.


The Budget Enforcer: cgroups Keep Containers in Check

Namespaces create isolation, but they don’t stop a container from eating all your resources. That’s where cgroups (control groups) come in.

Remember Part 1’s discussion about resource limits? cgroups are what actually enforce those limits. When I run:

docker run -m 512m --cpus=2 redis

Docker doesn’t enforce those limits—the Linux kernel does, using cgroups.

How cgroups Actually Work

On my Mac Mini, inside Docker’s Linux VM, the kernel creates a cgroup hierarchy:

/sys/fs/cgroup/
├── memory/
│   └── docker/
│       └── abc123container/
│           └── memory.limit_in_bytes = 536870912  (512 MB)
│
└── cpu/
    └── docker/
        └── abc123container/
            ├── cpu.cfs_quota_us = 200000   (2 CPUs)
            └── cpu.cfs_period_us = 100000

Every time the container tries to allocate memory or use CPU, the kernel checks these files. Exceed the memory limit? OOM killer terminates the process. Exceed CPU? Throttled.

The Google Origin Story

Here’s a fun fact: Google contributed cgroups to the Linux kernel in 2006. Why? They were running millions of processes across their infrastructure and needed a way to prevent one task from monopolizing resources.

Every Google search, YouTube video, Gmail message—all running in isolated cgroups. By the time Docker came along in 2013, cgroups had been battle-tested at planetary scale for 7 years.


The Filesystem Trick: chroot’s 45-Year History

The oldest piece of the container puzzle is chroot, which dates back to 1979—that’s 44 years before Docker was founded!

What chroot Does

Let me demonstrate this on a Linux system (remember, containers need Linux):

# Create a minimal root filesystem
mkdir -p /tmp/fake-root/{bin,lib,lib64}
cp /bin/bash /tmp/fake-root/bin/
cp /bin/ls /tmp/fake-root/bin/

# Copy required libraries
ldd /bin/bash  # Find dependencies
cp /lib/x86_64-linux-gnu/libtinfo.so.6 /tmp/fake-root/lib/
# ... copy other dependencies ...

# Now "change root" into this directory
chroot /tmp/fake-root /bin/bash

What just happened?

Inside this chroot environment, / is actually /tmp/fake-root on the host. I can’t see the real filesystem. I’m trapped in a fake root directory.

Modern Containers Use Overlay Filesystems

chroot was simple but inefficient. Modern containers use overlay filesystems:

┌─────────────────────────────────────┐
│  Container's view of /              │
├─────────────────────────────────────┤
│  Writable layer (your changes)      │  ← Gets destroyed when container stops
├─────────────────────────────────────┤
│  nginx:latest (top layer)           │  ← Read-only
├─────────────────────────────────────┤
│  Ubuntu base (middle layer)         │  ← Read-only, shared
├─────────────────────────────────────┤
│  Kernel modules (bottom layer)      │  ← Read-only, shared
└─────────────────────────────────────┘

Multiple containers can share the read-only layers. Only the writable top layer is unique per container. This is why pulling an image is fast after the first time—Docker reuses cached layers.


The Timeline: Containers Existed Long Before Docker

Here’s what blows people’s minds—containers have existed for decades:

YearTechnologyWhat Happened
1979chrootUnix V7 adds filesystem isolation
2000FreeBSD JailsFirst “complete” container system
2004Solaris ZonesSun Microsystems’ enterprise containers
2005OpenVZLinux containers for web hosting
2006cgroupsGoogle contributes resource control to Linux
2008LXCFirst mainstream Linux containers
2013DockerMakes containers easy for developers
2014KubernetesGoogle’s container orchestrator
2015OCIDocker donates runtime, industry standardizes
2017containerdDocker donates core runtime to CNCF
2018PodmanRed Hat’s daemonless alternative

The Company That Made Containers Mainstream

Docker (originally dotCloud, a PaaS company) didn’t invent containers. But in 2013, they made containers accessible to ordinary developers.

Before Docker (2012):

# Running a container with LXC required expertise
sudo lxc-create -t download -n mycontainer -- -d ubuntu -r focal -a amd64
sudo lxc-start -n mycontainer
sudo lxc-attach -n mycontainer
# Then manually configure networking, storage, volumes, etc.

After Docker (2013):

docker run ubuntu bash
# That's it. Everything just worked.

Docker’s genius was user experience:

  • Dockerfile - Reproducible image builds
  • Docker Hub - Public registry for sharing images
  • Simple CLI - One command to run anything
  • Portability - Same image runs on any Docker host

Why Understanding These Primitives Matters

When I’m debugging container issues, knowing these primitives helps me think clearly:

Scenario 1: Container Can’t See Other Containers’ Processes

Me: “Ah, that’s PID namespaces. Each container has its own PID namespace.”

Solution: Use docker exec to enter the container or share the PID namespace:

docker run --pid=container:other_container myimage

Scenario 2: Container Keeps Getting Killed with Exit Code 137

Me: “Exit 137 is OOM kill. The container exceeded its cgroup memory limit.”

Solution: Either increase the limit or fix the memory leak:

docker run -m 2g myapp  # Up from 1g

Scenario 3: Container Can’t Write to Mounted Directory

Me: “Probably a mount namespace + permissions issue. The container sees different UIDs.”

Solution: Fix ownership or use user namespace mapping:

chown -R 1000:1000 /my/volume  # Match container's user

The Bottom Line: Containers Are Just Restricted Processes

Let me drive this home with one final demonstration. I’ll run a Redis container and prove it’s just a process:

# Start Redis
docker run -d --name redis redis

# Find the process ID on my Mac's Docker VM
docker inspect redis --format '{{.State.Pid}}'
# Output: 48291

# Look at the cgroup limits
cat /sys/fs/cgroup/memory/docker/abc123.../memory.limit_in_bytes
# See the actual byte limit

# Check namespaces
ls -la /proc/48291/ns/
# Each file is a different namespace (pid, net, mnt, etc.)

It’s just a Linux process with:

  • Namespaces (fake view of PIDs, network, filesystem)
  • cgroups (resource limits enforced by kernel)
  • Overlayfs (efficient copy-on-write filesystem)

No hypervisor. No virtualization. Just kernel features.

This is why containers are so fast—they’re native processes. Startup time is milliseconds, not minutes. Memory overhead is kilobytes, not gigabytes.

Understanding these primitives means you truly understand containers. Everything else—Docker, Kubernetes, Podman—is just tooling built on top of these three Linux features.

Comments