The Docker Toolchain Deep Dive: CLI to Daemon to containerd to runc
Tracing the execution process chain behind a single docker run command.
Part 2 in Series — Catch up on the previous article: What Docker Adds on Top of Linux Kernel Isolation Primitives (Part 1) before diving into this post.
You log into a production host and execute a simple command:
docker run -d --name web-server -p 80:80 nginx
Within 800 milliseconds, the command returns a 64-character container ID, and Nginx starts serving web traffic.
Later, an engineer performs a rolling software update on the Docker system daemon:
systemctl restart docker
While the dockerd daemon restarts, not a single HTTP request to the running Nginx container drops. The web server keeps running uninterrupted.
How does Docker restart its main control daemon without killing active running containers?
To answer this, we must examine the multi-layered process architecture that executes underneath the docker command line interface: CLI dockerd containerd containerd-shim runc.
The Architecture of the Process Chain
Modern Docker is not a single monolithic process. It is a collection of modular tools connected via UNIX sockets and gRPC interfaces:
[ User Shell ]
|
| docker run nginx
v
+-------------------------------------------------------------------+
| 1. Docker CLI (/usr/bin/docker) |
+-------------------------------------------------------------------+
|
| HTTP REST API payload over /var/run/docker.sock
v
+-------------------------------------------------------------------+
| 2. Docker Daemon (/usr/bin/dockerd) |
| - Handles image builds, network bridges, volume management |
+-------------------------------------------------------------------+
|
| gRPC Requests over /run/containerd/containerd.sock
v
+-------------------------------------------------------------------+
| 3. containerd Daemon (/usr/bin/containerd) |
| - Manages image snapshots, container lifecycles, content store |
+-------------------------------------------------------------------+
|
| Forks a light daemonless shim process per container
v
+-------------------------------------------------------------------+
| 4. containerd-shim (/usr/bin/containerd-shim-runc-v2) |
| - Holds open stdin/stdout/stderr file descriptors |
| - Manages container exit status codes |
+-------------------------------------------------------------------+
|
| Executes OCI Bundle
v
+-------------------------------------------------------------------+
| 5. runc CLI (/usr/bin/runc) |
| - Sets up Linux namespaces & cgroups via libcontainer |
| - Executes entrypoint binary (PID 1) |
| - EXITS immediately after container creation! |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| 6. Containerized Process (nginx: master process) |
+-------------------------------------------------------------------+
1. Docker CLI (/usr/bin/docker)
The docker command is a client-side Go binary. It performs no container management or namespace creation itself.
When you type docker run, the CLI translates flags (-d, -p 80:80) into an HTTP JSON payload and transmits it across a local UNIX domain socket at /var/run/docker.sock to the Docker Daemon:
POST /v1.43/containers/create?name=web-server HTTP/1.1
Host: localhost
Content-Type: application/json
{
"Image": "nginx",
"ExposedPorts": { "80/tcp": {} },
"HostConfig": {
"PortBindings": { "80/tcp": [{ "HostPort": "80" }] }
}
}
2. Docker Daemon (dockerd)
The dockerd process acts as the high-level orchestrator for host-level container features:
- Build Engine: Translates Dockerfile instructions into image layers.
- Virtual Networking: Allocates IP addresses, updates iptables rules, and sets up virtual bridge interfaces (
docker0). - Volume Management: Mounts host directories or cloud block storage into container paths.
- REST API Server: Authenticates requests and exposes endpoints.
Once dockerd completes high-level network and storage preparation, it hands off container execution to containerd.
3. containerd
containerd is an industry-standard, CNCF-graduated container runtime daemon.
While dockerd handles high-level user features, containerd manages low-level container lifecycle execution:
- Pulling and pushing container images from registries.
- Unpacking image layers into root filesystems using OverlayFS snapshotters.
- Managing container execution state (start, stop, pause).
containerd listens on a gRPC socket at /run/containerd/containerd.sock. It can operate completely independent of Docker (for example, Kubernetes uses containerd directly via CRI without dockerd).
4. containerd-shim
When containerd is instructed to start a container, it does not launch the container process directly under its own process tree.
Instead, containerd forks an intermediate process called containerd-shim.
Why the Shim Exists:
- Daemonless Container Execution: The shim acts as the parent process for the container’s PID 1. If
dockerdorcontainerdcrash or undergo system upgrades, the shim keeps running, keeping the container process alive! - I/O Streaming: Holds open the standard I/O pipes (
stdin,stdout,stderr) so logs are captured even if the daemon restarts. - Exit Code Reporting: Waits for the container PID 1 process to terminate and reports its exit code back to
containerd.
5. runc
runc is the reference implementation of the Open Container Initiative (OCI) Runtime Specification.
When containerd-shim starts, it invokes runc create passing an OCI Bundle (a directory containing an config.json file and a rootfs/ directory).
How runc Creates the Container:
- Reads
config.jsonto extract namespace settings, cgroup limits, capabilities, and environment variables. - Invokes Linux system calls (
clone(2)orunshare(2)) to create new namespaces (PID, NET, MNT, UTS, IPC). - Configures cgroup resource limits in
/sys/fs/cgroup/. - Executes
pivot_root(2)to lock the process intorootfs/. - Executes
execve(2)to replace its own process memory with the target container binary (e.g.,nginx).
Crucially, once execve(2) replaces the process with Nginx, runc exits completely. It consumes zero system memory during container runtime!
Inspecting the Live Process Tree
You can observe this process chain directly on any Linux host using pstree:
$ pstree -plu $(pgrep containerd)
containerd(1042)─┬─containerd-shim(8920)───nginx(8945)───nginx(8990)
└─{containerd}(1043)
Notice the process tree hierarchy:
containerd(PID 1042) is the parent ofcontainerd-shim(PID 8920).containerd-shimis the parent of thenginxmaster process (PID 8945).runcis nowhere in the process tree because it exited after setup!dockerdis absent from the container process parentage, which is why restartingdockerddoes not disrupt Nginx.
Summary & Next Steps
Docker’s modular toolchain separates high-level developer APIs from low-level kernel isolation execution:
dockerCLI converts terminal commands into REST API requests sent over/var/run/docker.sock.dockerdorchestrates networks, volumes, and API requests.containerdmanages image snapshots and container lifecycles via gRPC.containerd-shimkeeps container processes alive independently of daemon restarts.runcexecutes system calls to set up namespaces/cgroups and exits.
In the next article, we examine The Open Container Initiative (OCI): Image Specification and Runtime Specification.
References & Further Reading
- Linux Foundation. OCI runc Runtime Spec & Command Line Interface. GitHub.
- Cloud Native Computing Foundation. CNCF containerd Architecture & gRPC Services Specification. containerd Docs.
- Docker Inc. Docker Engine Architecture and Daemon Socket Interface. Docker Docs.
Part 3: The Open Container Initiative (OCI): Image Specification and Runtime Specification
Continue to Part 3 →