Part I — Containers & Docker
1. What a container actually is
A container is not a lightweight VM. It is an ordinary Linux process that the kernel has been told to lie to. Three kernel features do the lying:
| Feature | What it isolates/limits | Seen from inside the container |
|---|---|---|
| Namespaces | what the process can see: PIDs, mounts, network, hostname (UTS), IPC, users, cgroups | "I am PID 1", "I have my own eth0", "/ is my image" |
| cgroups (v2) | what the process can use: CPU, memory, IO, PIDs | "I get killed at 256 MiB", "I get throttled above 0.5 CPU" |
| Layered filesystem (overlayfs) | a root filesystem assembled from read-only image layers + one writable layer | "/usr/bin/myapp exists", writes vanish when the container is removed |
Plus some hardening: capabilities (root split into ~40 privileges, most dropped), seccomp (syscall filter), and AppArmor/SELinux (mandatory access control).
VM Container
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ app │ │ app │ │ app │
│ libs │ │ libs (image) │ │ libs (image) │
│ guest kernel │ └──────┬───────┘ └──────┬───────┘
├──────────────┤ │ namespaces + cgroups
│ hypervisor │ ┌──────┴────────────────┴───────┐
├──────────────┤ │ ONE host kernel │
│ host kernel │ └───────────────────────────────┘
└──────────────┘
Consequences you will rely on later:
- Containers start in milliseconds because nothing boots; a process is just exec'd.
- The kernel is shared. A kernel exploit escapes every container on the host. That is why we drop capabilities, run as non-root, and use seccomp.
- On macOS/Windows, Docker runs a Linux VM behind the scenes. Your containers share that VM's kernel, not macOS's.
- Memory limits are enforced by the kernel OOM killer, not by your
runtime. Exceed the cgroup limit and the process gets
SIGKILL: exit code 137, no chance to clean up.
1.1 Build a "container" by hand (Linux only)
Doing this once makes everything else make sense. On a Linux VM:
# 1. get a root filesystem (an "image") - here, Alpine's minirootfs
mkdir -p /tmp/rootfs && cd /tmp/rootfs
curl -sSL https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-minirootfs-3.20.3-x86_64.tar.gz | tar xz
# 2. start a shell in new PID, mount, UTS, IPC and network namespaces, chrooted into it
sudo unshare --pid --fork --mount-proc=/tmp/rootfs/proc \
--mount --uts --ipc --net \
chroot /tmp/rootfs /bin/sh
# inside:
hostname orbit-box && hostname # changes only this UTS namespace
ps aux # you are PID 1; host processes are invisible
ip addr # only a down loopback: new network namespace
Now add a cgroup memory limit from another terminal on the host:
sudo mkdir /sys/fs/cgroup/demo
echo 50M | sudo tee /sys/fs/cgroup/demo/memory.max
echo <PID-of-the-unshare-shell> | sudo tee /sys/fs/cgroup/demo/cgroup.procs
# inside the box, allocate 100 MB and watch the kernel kill it:
# head -c 100m /dev/zero | tail -> Killed
That is, give or take, what runc does when Docker or Kubernetes starts a
container.
⚠️ Caveat (verify): the Alpine URL pins
3.20.3andx86_64. If it 404s, take the current file name fromdl-cdn.alpinelinux.org/alpine/latest-stable/releases/<arch>/. In the Apple Silicon lab VM (§0), useaarch64. The cgroup path assumes cgroup v2 (stat -fc %T /sys/fs/cgroupprintscgroup2fs).
1.2 The standards: OCI, containerd, runc
docker CLI ──► dockerd ──► containerd ──► containerd-shim ──► runc ──► your process
kubectl ──► API server ──► kubelet ──CRI──► containerd ──► shim ──► runc ──► your process
- OCI image spec: what an image is (a manifest + config + tarball layers, content-addressed by SHA-256). Any OCI image runs on any OCI runtime: Docker, containerd, CRI-O, Podman.
- OCI runtime spec: how to run one (
runc,crun,gVisor'srunsc, Kata Containers). - CRI: the gRPC interface the kubelet uses to talk to a runtime.
Kubernetes removed its Docker shim in 1.24; clusters run containerd or
CRI-O directly. Images built with
docker buildwork unchanged because they are OCI images.
Real-world example: "We removed Docker from our nodes, will our images break?" was a common panic in 2022. They didn't: only things that talked to
/var/run/docker.sockon the node (old log agents, Docker-in-Docker CI) broke. Your build tool and your node runtime are independent choices.