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:
| Namespace | Year Added | What It Fakes |
|---|---|---|
| pid | 2002 | Process IDs - Makes your container think it’s PID 1 |
| net | 2009 | Network stack - Own IP address, ports, routing tables |
| mnt | 2002 | Filesystem mounts - Gets its own root / directory |
| uts | 2006 | Hostname - Container can be “webserver-1” while host is “mac-mini” |
| ipc | 2006 | Inter-process communication - Isolated shared memory |
| user | 2012 | User IDs - “root” in container = unprivileged user on host |
| cgroup | 2016 | Resource visibility - Container sees only its own limits |
| time | 2020 | System 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:
| Year | Technology | What Happened |
|---|---|---|
| 1979 | chroot | Unix V7 adds filesystem isolation |
| 2000 | FreeBSD Jails | First “complete” container system |
| 2004 | Solaris Zones | Sun Microsystems’ enterprise containers |
| 2005 | OpenVZ | Linux containers for web hosting |
| 2006 | cgroups | Google contributes resource control to Linux |
| 2008 | LXC | First mainstream Linux containers |
| 2013 | Docker | Makes containers easy for developers |
| 2014 | Kubernetes | Google’s container orchestrator |
| 2015 | OCI | Docker donates runtime, industry standardizes |
| 2017 | containerd | Docker donates core runtime to CNCF |
| 2018 | Podman | Red 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