Frontend Labs
⋯

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.3 and x86_64. If it 404s, take the current file name from dl-cdn.alpinelinux.org/alpine/latest-stable/releases/<arch>/. In the Apple Silicon lab VM (§0), use aarch64. The cgroup path assumes cgroup v2 (stat -fc %T /sys/fs/cgroup prints cgroup2fs).

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's runsc, 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 build work 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.sock on the node (old log agents, Docker-in-Docker CI) broke. Your build tool and your node runtime are independent choices.


Generated from real-life-k8s-guide.md · 8 Oct 2026 16:36