Docker & ContainersI · FoundationsArchitecture
Docker architecture at a glance
What you'll learn
- Map every daemon and runtime that participates in a container lifecycle
- Explain that the docker CLI is an HTTP client, and what follows from that
- Explain why the shim exists
- Predict what a restart of each component does to running containers
- Identify which component to investigate for which class of failure
Prerequisites
None — start here.
Verified against Docker Engine 29.x · Docker Engine 28.x · Docker Compose 2.x · containerd 2.x · runc 1.2.x · BuildKit 0.20+ · Linux kernel 5.15+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-12
Five processes run on a Docker host for every container that runs. Knowing which one is responsible for which failure is half of production Docker troubleshooting.
The five processes
sequenceDiagram
participant CLI as docker CLI
participant Dockerd as dockerd (daemon)
participant Containerd as containerd
participant Shim as containerd-shim
participant Runc as runc
participant Kernel as Linux kernel
CLI->>Dockerd: docker run nginx (REST over socket)
Dockerd->>Containerd: Create container (gRPC)
Containerd->>Containerd: Pull image (if needed)
Containerd->>Shim: Start container
Shim->>Runc: create + start (OCI)
Runc->>Kernel: clone(2) + execve(2)
Kernel-->>Runc: PID of init process
Runc-->>Shim: container PID
Shim-->>Containerd: container running
Containerd-->>Dockerd: container running
Dockerd-->>CLI: container started
1. docker (the CLI)
A thin client. Does not store state; every call is request/response
to dockerd. Talks to the daemon over the configured context
(DOCKER_HOST), defaulting to /var/run/docker.sock.
“Thin client” understates it. The docker binary is an HTTP
client, and the Docker Engine API is an ordinary REST API — the
documentation says it can be reached with “an HTTP client such as
wget or curl, or the HTTP library which is part of most modern
programming languages”, with the API version as the first path
element: “if the endpoint is /containers/ you can use
/v1.55/containers/”.
You can prove this in one command:
# curl -s --unix-socket /var/run/docker.sock http://localhost/v1.49/containers/json | jq -r '.[].Names[0], .[].State'/web
/redis
/postgres
running
running
runningIllustrative output
docker context ls
docker context show
printenv DOCKER_HOST || echo 'DOCKER_HOST is unset; the default socket is in use'2. dockerd (the daemon)
Long-running process. Manages images, volumes, networks, builds,
and the userland proxy (docker-proxy, one process per published
port on the default configuration). Forwards container lifecycle
requests to containerd. Reads /etc/docker/daemon.json for
configuration.
What dockerd does not do:
- Create namespaces.
- Apply cgroups.
- Run containers.
On Engine 28.x with the classic image store, dockerd owns the
image store itself and hands containerd only the job of running
things. From Engine 29.0 the containerd image store is the
default for fresh installations, which moves image storage down
to containerd too. Which of the two you are on determines whether
ctr -n moby images ls shows your images or an almost-empty list —
a genuinely confusing difference if you do not know it exists.
3. containerd (the supervisor)
Long-running process, started as its own systemd unit. Manages
container lifecycle independently of dockerd: it holds the content
store and snapshotter, the container metadata, and the task
service that starts and supervises processes. Docker’s containers
live in containerd’s moby namespace, which is why every ctr
command in this course carries --namespace moby.
4. containerd-shim-<id> (one per container)
A small process that exists so that containerd can be restarted without taking containers down. The shim holds the parent of the container’s PID 1, so if containerd dies and is restarted, the shim is still around to keep the container alive.
You will see one shim per container in ps auxf.
5. runc (or crun, or youki)
Short-lived. This is the part people picture as “the container runtime” and then imagine as a daemon; it is neither long-running nor a service. It is a binary the shim executes, once per operation.
Given a bundle — a directory containing a config.json written
to the OCI runtime spec and a rootfs/ — runc does the actual
isolation work: creates the namespaces, applies the cgroup limits,
sets the capability bounding set, loads the seccomp filter and the
LSM label, pivot_roots into the rootfs, and execves the
entrypoint as PID 1 of the new PID namespace. Then it exits. The
shim takes over.
CONTAINER=web
ID=$(docker inspect --format '{{.Id}}' "$CONTAINER")
runc --root /run/docker/runtime-runc/moby list
cat /run/containerd/io.containerd.runtime.v2.task/moby/"$ID"/config.json | jq '.linux.namespaces'What a restart of each component costs
This is the table to internalise, because it is the difference between a change you can make at 14:00 and one that needs a window.
| Restart | Running containers | Networking | docker exec sessions | Logs |
|---|---|---|---|---|
docker CLI | Nothing — it is a client | Nothing | Nothing | Nothing |
containerd | Survive — shims are separate processes | Untouched | Dropped | Buffered by the shim |
dockerd without live restore | All stopped | Rules re-applied | Dropped | Gap |
dockerd with live restore | Survive | Rules re-applied | Dropped | Buffered, then drained |
Why this architecture matters for ops
| Symptom | First place to look |
|---|---|
| CLI errors with “Cannot connect to the Docker daemon” | dockerd (is it running? socket permissions?) |
| CLI errors with “connection refused” but daemon is running | socket path, DOCKER_HOST, or the selected docker context |
Container exists in docker ps but won’t start | containerd (image pull, shim, runtime) |
Container exists but docker exec fails | shim (process supervisor issue) |
| Kernel-level errors (cgroup, capability, namespace) | runc (and the kernel) |
| Containers vanished after a package upgrade | live-restore was off when dockerd restarted |
docker images empty but containers run | classic vs containerd image store mismatch in your tooling |
Inspecting the running stack
ps -eo pid,comm | grep -E 'dockerd|containerd|runc|shim' | head -20ps -eo pid,ppid,comm | awk '$3 ~ /containerd-shim/'ctr --namespace moby containers ls
ctr --namespace moby tasks ls$ docker version --format '{{.Server.Version}} containerd={{.Server.Components}}'28.3.2 containerd=[{Engine 28.3.2 map[...]} {containerd 1.7.27 map[...]} {runc 1.2.6 map[...]} {docker-init 0.19.0 map[...]}]Illustrative output
Knowledge check
Knowledge check · 6 questions
Q1. Which component actually creates the Linux namespaces that isolate a container?
Q2. On a default Docker installation with no daemon.json changes, what happens to running containers when you run `systemctl restart docker`?
Q3. Why is adding a user to the `docker` group equivalent to granting them root on that host?
Q4. The `docker` command performs container creation itself and needs its own privileges to do so.
Q5. Which of these survive a restart of `containerd` alone? Select all that apply.
Q6. Why does each container have its own containerd-shim process?
Passing score: 75%. Answers are checked in this browser.