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
Not a VM — No Separate Kernel

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.

Host Kernel Cgroups boundary (CPU · Memory · I/O) Namespaces PID · Net · Mount UTS · IPC · User Container Process thinks it is PID 1 on its own machine
Fig 1. A container process lives inside namespace isolation, bounded by cgroups, sharing the host kernel.
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
Which namespace makes a container process believe it is PID 1?
  • The net namespace
  • The pid namespace
  • The mnt namespace
  • Cgroups handle PID numbering

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
Industry: Kubernetes OOM Killer Uses Cgroups

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.

A container is consuming 100% of the host CPU, slowing other workloads. What flag controls this?
  • --isolate-cpu
  • --namespace=cpu
  • --cpus=<value> (cgroup CPU quota)
  • CPU limits require a VM, not a container

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.

Container Layer Read-Write · copy-on-write ↑ mount (OverlayFS) ↑ App Code Layer Read-Only App Dependencies Layer Read-Only Base OS Layer Read-Only (e.g. debian:slim)
Fig 2. Union filesystem stack. All image layers are read-only; only the container layer accepts writes. Deleted on 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.

You write a 100 MB file inside a running container. Where does it go?
  • Into the base image layer, modifying it for future containers
  • Into the container's writable layer — deleted when the container is removed
  • Into a Docker-managed volume automatically
  • Nowhere — containers have a read-only filesystem

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:

1. fork() 2. unshare() namespaces 3. write cgroup limits 4. mount OverlayFS 5. exec() nginx Container running ✓
Fig 3. runc start sequence for 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.

Task 1 — Find the Container PID on the Host
# 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.

Task 2 — Inspect Namespace File Descriptors
# 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.

Task 3 — Test Memory Limits
# 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.

You have two containers running from the same nginx image. How many copies of the nginx image layers exist on disk?
  • Two — one per container
  • One shared copy; each container gets its own separate writable layer
  • Zero — layers are streamed from the registry at runtime
  • It depends on the storage driver

🔑 Key Takeaways

  • A container is a process. docker top and ps on 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, and docker inspect.