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 runis a REST call todockerd, which drivescontainerd, acontainerd-shim-runc-v2and the OCI runtimerunc.runccallsclone()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 hostand--uts hostshare 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.maxbandwidth control throttles a container to its quota per period (cpu.statcountsnr_throttled/throttled_usec);cpu.weightonly shares CPU under contention.--cpu-sharesbecomescpu.weightby runc’s formula1 + (shares − 2) × 9999 / 262142, and--blkio-weightbecomesio.weightby1 + (weight − 10) × 9999 / 990.memory.maxplusmemory.swap.maxdrive the OOM killer, which kills a process in that cgroup only (memory.eventsoom_kill).pids.maxmakes a fork bomb’sfork()`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 isupperdir, and the container sees themergedview. 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
RUNwhose inputs didn’t change, are reused. Order instructions by how often their inputs change (copyrequirements.txtand install before copying the whole source). -
Networking (bridge networking): each container gets a veth pair onto a bridge (
docker0, or a per-networkbr-…) and an address from the network’s pool. A user-defined network has embedded DNS, so containers reach each other by name; the defaultbridgedoes not. Outbound traffic isMASQUERADE`d to the host IP; a published port (-p 8080:80`) is aDNATrule in netfilter’snattable that rewrites the destination to the container.--network noneis loopback only;--network hostshares the host’s stack. -
Volumes (volumes): data on a volume survives
docker rm; the writable layer does not;--tmpfslives 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-onlymakes the rootfs read-only; user namespaces / rootless mode make container root unprivileged on the host (rootless).--privilegedand a mounteddocker.sockare effectively root on the host.
Limits
-
One host, one registry, a handful of images and containers. The simulator does not run real programs: a
RUNstep 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
nattable is the same in 28). No IPv6, no--userland-proxy=falsenuances 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 |
|---|---|---|
|
A scenario (list below), with its own starting state; parameters given with it override it |
|
|
The same, without the scenario’s own settings being written into the link |
|
|
The diagram view: |
|
|
CPUs the host has, 1–16 |
|
|
Host architecture for pulls: |
|
|
Message latency in ms (how long a hop takes in simulated time) |
|
|
Show the shim and each kernel step ( |
|
|
Random seed (container name generation) |
|
|
Playback speed: |
|
|
Hop time in seconds ( |
|
Scenarios:
example= |
Shows |
|---|---|
|
A container is just a process: docker run → dockerd → containerd → runc → clone(), PID 1 inside vs a high PID on the host (open) |
|
Namespaces: toggle pid, mnt, net, uts, ipc, user, cgroup and see what the process can see (open) |
|
cgroups: CPU throttling on a timeline, memory.max → OOM killer, pids.max contains a fork bomb, io.weight (open) |
|
Image layers and the union filesystem: reads find the top layer, writes copy up, deletes whiteout, two containers share the image (open) |
|
Building an image and the cache: a layer per step with content digests; change a file or reorder and watch what rebuilds (open) |
|
Registries and pulling: tag → index → manifest → config + layers, pull only missing blobs, verify digests, pin by digest (open) |
|
Networking: docker0 and veths, container DNS on a user network, outbound masquerade, a published port DNAT’d through iptables, host and none (open) |
|
Volumes and bind mounts: data that survives docker rm vs the writable layer that doesn’t; tmpfs (open) |
|
Security: dropped capabilities, seccomp, read-only rootfs, user namespaces, and why --privileged and docker.sock are dangerous (open) |
|
An empty host to experiment with from the terminal (open) |
Feedback is a gift. Something to improve, a feature to add, or a bug? Tell me.
Runs entirely in your browser; nothing you type leaves this page.