Chris Collins

Tools · containers

How containers work, simulated

An animated, step-by-step Linux host running Docker in your browser: docker run through dockerd, containerd and runc to clone(); namespaces, cgroups and the OOM killer; overlayfs copy-up and whiteouts; the build cache; pulling by digest from a registry; the bridge, veths, DNS and NAT; volumes; and the capabilities, seccomp and user namespaces that keep a container in — with a terminal that drives the same diagram.

Added

How to use it

A Linux host in miniature that runs entirely in this page: dockerd, containerd and runc, a registry, a few containers, their namespaces, cgroups, layered filesystems and a bridge network. Every command you run — from the scenarios or by hand in the terminal — moves as dots between the daemon, the kernel, the registry and the containers, and every rule (what a process can see, what it may use, where a file lives, which packet goes where, which syscall is allowed) follows how Docker and the Linux kernel actually behave. Nothing here talks to real Docker; to lint real Compose and Kubernetes files, use the Compose/Kubernetes checker.

How to use it

Pick a scenario and press Start. Each step runs a command or two, then plays for a few simulated seconds and pauses so you can read what happened and why. Next step moves on, Back rewinds (the simulator is deterministic, so it replays the earlier steps), ⏭ jumps to the end of the step and Step ⇥ advances to the next single thing that happens. Space plays and pauses; → is the next step, ← the previous. The Playground scenario has no script — drive it from the terminal.

Speed and Hop set the pace. Speed scales everything. Hop is how long each drawn message takes to cross a wire at 1×; while one is travelling the simulated clock slows so you can follow it, and quiet stretches pass at full speed. Sim clock turns the slowing off. Hop changes only how the run is shown, never what happens. With reduced motion set in your system, messages jump instead of travelling.

⛶ Full screen (or F) gives the simulator the whole screen — the browser’s full screen where it is allowed, otherwise a full-window layer (iPhone). The diagram fills the space and is redrawn as the window changes; on a wide screen the controls and panels sit in a column beside it, and ◫ Panels hides them (the View picker stays) so the diagram goes edge to edge. Escape leaves.

  • The diagram. A system diagram drawn from the simulator’s state. Along the top is the chain a command walks — you → docker CLI → dockerd → containerd → (shim →) runc — above a box for the Linux kernel with its subsystems (namespaces, cgroups, the overlay rootfs, the capabilities/seccomp/LSM gates, netfilter). Below are the containers and, depending on the View, their process trees, namespaces, cgroup meters, layer stacks, the build, the pull, the network topology or the security gates. Messages travel the wires as labelled packets, coloured by kind (daemon/API, kernel syscall, image blob, filesystem, packet, NAT rewrite, DNS, denied). Tap a container to inspect it and act on it (stop, kill, exec, start, remove). Turn on detail (below the terminal) to see the shim and each separate kernel step.

  • View switches what the main diagram shows without changing the simulation: Processes (the host’s PIDs vs the container’s), Namespaces (what each namespace lets the process see, with toggles to share one with the host), cgroups (a CPU timeline with throttling, a memory bar to the OOM point, a pids bar), Filesystem (the overlay layer stack, copy-up and whiteouts), Build (the Dockerfile step by step with cache hits and layer digests), Registry (tag → index → manifest → config + layers, with per-layer status), Network (docker0/user bridges, veths, the netfilter chains, published ports) and Security (the seccomp → capabilities → LSM gates).

  • Event log records what the kernel and daemon did; Key events hides the chatter.

  • Terminal accepts a small set of commands that drive the same diagram (see below).

The terminal

On the host it takes: docker run (with -d, -it, --name, -p, -v, --tmpfs, --network/--net, -h, -m/--memory, --memory-swap, --cpus, --cpu-shares, --pids-limit, --blkio-weight, --device-write-bps, --cap-add, --cap-drop, --security-opt, -u, --pid/--ipc/--uts/--userns host, -e, -w, --read-only, --privileged, --init, --rm, --platform, --entrypoint), docker create, ps, start, stop, kill, restart, rm, exec, logs, inspect, top, stats, build, pull, push, tag, images, network create|ls|rm|inspect, volume create|ls|rm|inspect, version and info. After docker exec <c> … (or in the shell a -it container drops you into), it runs in-container commands: ps, hostname, id, cat, ls, cd, echo … > file, rm, mkdir, touch, cp, mv, chown, mount, unshare, sysctl, date -s, modprobe, keyctl, ip, curl, wget, ping, nslookup, free, nproc, and pipes into grep/head/tail/wc. The sample images are nginx, python:3.12-slim, postgres:16, debian, alpine and docker.

What it models

  • A container is a process. docker run is a REST call to dockerd, which drives containerd, a containerd-shim-runc-v2 and the OCI runtime runc. runc calls clone() with new namespaces, sets up the cgroup, mounts the rootfs and `pivot_root`s, drops capabilities, applies seccomp, and `execve()`s your program as PID 1. PID 1 in the container is an ordinary high numbered PID on the host — the same process, two numbers (pid_namespaces(7)).

  • Namespaces (namespaces(7)): pid, mnt, net, uts, ipc, user, cgroup. Each narrows one global resource. --pid host, --network host, --ipc host and --uts host share one with the host. User-namespace remapping maps container uid 0 to an unprivileged host uid (user_namespaces(7)).

  • cgroup v2 (kernel: cgroup v2): cpu.max bandwidth control throttles a container to its quota per period (cpu.stat counts nr_throttled/throttled_usec); cpu.weight only shares CPU under contention. --cpu-shares becomes cpu.weight by runc’s formula 1 + (shares − 2) × 9999 / 262142, and --blkio-weight becomes io.weight by 1 + (weight − 10) × 9999 / 990. memory.max plus memory.swap.max drive the OOM killer, which kills a process in that cgroup only (memory.events oom_kill). pids.max makes a fork bomb’s fork()`s return `EAGAIN; without it the host’s task budget runs out.

  • overlayfs (kernel: overlayfs, overlay2 driver): the image layers are lowerdir, the container’s writable layer is upperdir, and the container sees the merged view. A read returns the top-most copy; writing a lower file copies it up first; deleting a lower file writes a whiteout; a directory recreated over a whiteout is opaque. Two containers from one image share every read-only layer.

  • Images are OCI structures. A layer is bytes whose SHA-256 is its DiffID; layers chain by ChainID = sha256(parent + " " + diffID) (OCI image config). The config and manifest are JSON documents addressed by the SHA-256 of their bytes; a multi-arch tag points to an image index that lists one manifest per platform (OCI image index). A pull fetches only the layers it doesn’t already have and verifies every blob’s digest (OCI distribution spec); pinning by digest is reproducible.

  • The build cache (build cache): each instruction’s cache key is its parent’s key plus the instruction text, and for COPY/ADD the checksum of the files it copies. Change a copied file and that step and everything after it rebuild; the steps before it, and a RUN whose inputs didn’t change, are reused. Order instructions by how often their inputs change (copy requirements.txt and install before copying the whole source).

  • Networking (bridge networking): each container gets a veth pair onto a bridge (docker0, or a per-network br-…) and an address from the network’s pool. A user-defined network has embedded DNS, so containers reach each other by name; the default bridge does not. Outbound traffic is MASQUERADE`d to the host IP; a published port (-p 8080:80`) is a DNAT rule in netfilter’s nat table that rewrites the destination to the container. --network none is loopback only; --network host shares the host’s stack.

  • Volumes (volumes): data on a volume survives docker rm; the writable layer does not; --tmpfs lives only in RAM. A bind mount maps a host path straight in.

  • Security (Docker security): Docker keeps ~14 of ~40 capabilities, applies a default seccomp profile (which gates many syscalls on a capability and blocks the keyring outright), and an AppArmor profile. A syscall passes seccomp, then the capability check, then the LSM. --read-only makes the rootfs read-only; user namespaces / rootless mode make container root unprivileged on the host (rootless). --privileged and a mounted docker.sock are effectively root on the host.

Limits

  • One host, one registry, a handful of images and containers. The simulator does not run real programs: a RUN step in a build and a container’s main process have their filesystem and network effects modelled, not executed, so file contents are stand-ins while the structure (layers, digests, rules, PIDs) is real.

  • Timeouts are compressed so failures play out in seconds; the stop timeout is 1 s, not Docker’s 10 s. cgroup accounting is a simple, deterministic model, not a benchmark.

  • Netfilter is drawn as Docker Engine ≤ 27 writes it (the nat table is the same in 28). No IPv6, no --userland-proxy=false nuances beyond the hairpin case, no overlay/swarm networking.

  • The seccomp model gates the syscalls the scenarios use; it is not the full ~44-call profile.

Linking to an example

Links can be written by hand. A link’s settings replace this page’s saved settings (a button restores yours).

Parameter Meaning Example

example

A scenario (list below), with its own starting state; parameters given with it override it

example=cgroups

scenario

The same, without the scenario’s own settings being written into the link

scenario=networking

view

The diagram view: proc, ns, cg, fs, build, pull, net, sec

view=net

cpus

CPUs the host has, 1–16

cpus=2

arch

Host architecture for pulls: amd64 or arm64

arch=arm64

lat

Message latency in ms (how long a hop takes in simulated time)

lat=40

detail

Show the shim and each kernel step (true/false)

detail=true

seed

Random seed (container name generation)

seed=7

speed

Playback speed: 0.25, 0.5, 1, 2, 4 or 8

speed=2

hop

Hop time in seconds (0.25, 0.5, 1, 2, or clock for no pacing). Default 0.5

hop=1, hop=clock

Scenarios:

example= Shows

process

A container is just a process: docker run → dockerd → containerd → runc → clone(), PID 1 inside vs a high PID on the host (open)

namespaces

Namespaces: toggle pid, mnt, net, uts, ipc, user, cgroup and see what the process can see (open)

cgroups

cgroups: CPU throttling on a timeline, memory.max → OOM killer, pids.max contains a fork bomb, io.weight (open)

layers

Image layers and the union filesystem: reads find the top layer, writes copy up, deletes whiteout, two containers share the image (open)

build

Building an image and the cache: a layer per step with content digests; change a file or reorder and watch what rebuilds (open)

registry

Registries and pulling: tag → index → manifest → config + layers, pull only missing blobs, verify digests, pin by digest (open)

networking

Networking: docker0 and veths, container DNS on a user network, outbound masquerade, a published port DNAT’d through iptables, host and none (open)

volumes

Volumes and bind mounts: data that survives docker rm vs the writable layer that doesn’t; tmpfs (open)

security

Security: dropped capabilities, seccomp, read-only rootfs, user namespaces, and why --privileged and docker.sock are dangerous (open)

playground

An empty host to experiment with from the terminal (open)

Runs entirely in your browser; nothing you type leaves this page.