A container is not a tiny VM. It is a regular Linux process with three kernel features applied: namespaces, cgroups, and a union filesystem. This lesson shows you exactly what that means, provably.
1. A Container Is Just a Process
Run docker run -d nginx, then immediately run docker top <id> on the host. You will see a plain nginx process with a regular PID. There is no hypervisor, no separate kernel, no hardware virtualisation — just a Linux process the kernel launched with some extra flags.
# On the host:
$ docker run -d --name web nginx
$ docker top web
UID PID PPID CMD
root 18432 18410 nginx: master process nginx -g daemon off;
www 18487 18432 nginx: worker process
# Same PID visible in the host process table:
$ ps aux | grep 18432
A VM boots its own kernel. A container shares the host kernel — the exact same running kernel instance. There is no boot sequence, no kernel image inside the container image, and no hypervisor layer. That is why containers start in milliseconds and a 5 MB image is realistic.
2. Namespaces — The Isolation
The kernel feature that makes a process feel isolated is the namespace. Each namespace wraps one aspect of the global system and gives the process its own private view of it. Docker applies six namespaces at container start.
| Namespace | Isolates | Effect inside container |
|---|---|---|
pid |
Process IDs | First process is PID 1; can't see host PIDs |
net |
Network stack | Own eth0, IP address, routing table |
mnt |
Filesystem mounts | Sees only its own filesystem tree |
uts |
Hostname / domain | Own hostname (default: container ID) |
ipc |
IPC objects | Isolated shared memory, semaphores |
user |
UID/GID maps | UID 0 inside → unprivileged UID on host |
3. Cgroups — Resource Limits
Control groups (cgroups) answer a different question: not what can the process see, but how much of the machine can it consume. The kernel enforces hard limits on CPU time, RAM, disk I/O, and number of processes.
# Cap memory at 512 MB and CPU at 1.5 cores:
$ docker run --memory=512m --cpus=1.5 nginx
# Inspect actual cgroup settings:
$ docker inspect web --format '{{.HostConfig.Memory}}'
536870912 # 512 × 1024 × 1024 bytes
$ docker inspect web --format '{{.HostConfig.NanoCpus}}'
1500000000 # 1.5 × 10^9 nanocpus
When a Kubernetes pod exceeds its resources.limits.memory, the Linux OOM killer terminates the process. This is the cgroup memory limit triggering the kernel's out-of-memory handler — not Kubernetes code. The pod shows OOMKilled in kubectl describe pod. Same mechanism, same kernel feature you're learning right now.
4. Union Filesystem — Layers
Container filesystems are built from stacked read-only image layers with a single writable layer on top. The kernel stitches them into one apparent filesystem using a union mount (OverlayFS on modern Linux). Reads fall through to whichever layer owns the file; writes go to the top layer only — copy-on-write.
docker rm.Practical consequences: image layers are shared between containers that use the same base — ten nginx containers each get their own writable layer but share one copy of the nginx image layers on disk. And writes are ephemeral — the container layer vanishes with docker rm.
5. Putting It All Together: docker run nginx
When you run docker run nginx, runc executes these steps in order — all within a fraction of a second:
docker run nginx. Total elapsed: <100 ms on a modern host.6. Proving It: Inspect the Kernel Structures
Every claim above is verifiable from the host without any special tooling — the Linux /proc filesystem exposes it all.
# Start a container
$ docker run -d --name probe nginx
# Ask Docker for the host PID
$ docker inspect probe --format '{{.State.Pid}}'
18432
# Confirm it in the host process table
$ ps -p 18432 -o pid,ppid,cmd
The PID you see is a real kernel PID — the same one the OOM killer would target if memory limits were breached.
# Using the PID from Task 1:
$ PID=$(docker inspect probe --format '{{.State.Pid}}')
# List the namespace symlinks
$ sudo ls -la /proc/$PID/ns/
lrwxrwxrwx ipc -> ipc:[4026532305]
lrwxrwxrwx mnt -> mnt:[4026532303]
lrwxrwxrwx net -> net:[4026532308]
lrwxrwxrwx pid -> pid:[4026532306]
lrwxrwxrwx uts -> uts:[4026532304]
lrwxrwxrwx user -> user:[4026531837]
# Compare with a host process — different inode numbers = different namespace
$ sudo ls -la /proc/1/ns/
The inode numbers differ from the host's PID 1. That numerical difference is the isolation — the container is in a different namespace instance for each resource.
# Start a container limited to 64 MB
$ docker run -d --name memtest --memory=64m nginx
# Check the cgroup limit (cgroups v2 path):
$ PID=$(docker inspect memtest --format '{{.State.Pid}}')
$ sudo cat /proc/$PID/cgroup
0::/system.slice/docker-<id>.scope
$ sudo cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.max
67108864 # 64 × 1024 × 1024
# Or use docker stats:
$ docker stats memtest --no-stream
The 64 MB value in memory.max is the cgroup hard limit. Exceed it and the kernel OOM-kills the process — exactly what Kubernetes reports as OOMKilled.
🔑 Key Takeaways
- A container is a process.
docker topandpson the host both show it. No separate kernel, no boot sequence. - Namespaces create the illusion of isolation — six dimensions: PID, net, mnt, uts, ipc, user. Each is a kernel structure with a different inode number visible in
/proc/<pid>/ns/. - Cgroups enforce resource limits — CPU, memory, I/O, PIDs. The same mechanism Kubernetes uses when it reports
OOMKilled. - Union filesystems (OverlayFS) stack layers — read-only image layers shared across containers; one thin writable layer per container, discarded on
docker rm. - The full start sequence: fork → unshare namespaces → write cgroup limits → mount OverlayFS → exec. All in under 100 ms.
- Everything is verifiable from
/proc/<pid>/ns/,/proc/<pid>/cgroup, anddocker inspect.