Docker 2 🐳 Docker Engine Architecture: Daemon (dockerd), CLI (docker), containerd, and runC
Docker presents itself as a single command, but behind that command lies a layered architecture of cooperating processes. The Docker CLI you type into is only the outermost layer, a thin client that translates your input into API calls. Those calls reach the Docker daemon, which handles Docker-specific concerns like networking and volumes before delegating the actual container lifecycle to a separate runtime called containerd. containerd, in turn, calls a low-level OCI runtime named runc to create the isolated process that becomes your container.
This layering is not accidental complexity. It is the result of a deliberate architectural evolution that began when Docker extracted its core container runtime into standalone, reusable components. The modular design allows Kubernetes to bypass Docker entirely and talk directly to containerd. It allows alternative runtimes like youki or gVisor to replace runc without changing the Docker daemon. And it allows the Docker daemon itself to be restarted without killing running containers, because a shim process keeps them alive independently.
This chapter traces the request path from the docker CLI through dockerd, containerd, and runc, explaining what each component does, why it exists as a separate piece, and how the Open Container Initiative standards hold the stack together. We will also examine the containerd-shim, the small but crucial process that decouples container lifetimes from daemon lifetimes.
Key point: Docker’s architecture is a layered stack: the CLI sends REST API calls to the daemon, the daemon delegates container lifecycle management to containerd, and containerd uses runc to create the actual container process using Linux kernel primitives.
Why the layered architecture exists
The monolithic origins. Docker 1.0 was a single daemon that did everything: image management, networking, volume handling, and container execution were all baked into one binary. This worked, but it made Docker difficult to reuse. If another system wanted to run containers with Docker’s runtime, it had to adopt the entire Docker daemon with all its application-level concerns. The container runtime was inseparable from the container platform.
The modularization movement. Between 2015 and 2017, Docker broke its monolithic daemon into layers. The core runtime functionality became containerd, a standalone daemon that manages container lifecycle, image distribution, and storage. The low-level execution piece became runc, a small CLI that interfaces directly with the kernel. Docker donated both projects to open foundations: runc to the Open Container Initiative, and containerd to the Cloud Native Computing Foundation . This modularization allowed the broader ecosystem to build on the same foundations without adopting Docker’s entire stack.
The Kubernetes problem. Kubernetes originally used Docker as its container runtime, but the coupling was awkward. Kubernetes needed container lifecycle management, not image building or Docker networking or the Docker CLI. By extracting containerd, Docker created a component that Kubernetes could talk to directly through the Container Runtime Interface. Kubernetes 1.24 removed the Docker shim entirely, communicating with containerd (or CRI-O) instead . The modular design made this transition possible without rewriting the underlying runtime.
The daemonless container problem. If the Docker daemon managed every container’s lifecycle directly, restarting or upgrading the daemon would kill all running containers. The containerd-shim solves this by acting as the container’s parent process, independent of both dockerd and containerd. When dockerd restarts, the shim keeps the container running and reconnects when the daemon comes back . This makes container lifecycles resilient to management-plane restarts.
The standards problem. Before the Open Container Initiative, container formats and runtimes were fragmented. Each platform had its own image format and runtime interface. The OCI defined two specifications: the Runtime Specification, which describes how to run a container from a filesystem bundle, and the Image Specification, which describes how images are formatted and distributed . runc is the reference implementation of the Runtime Specification. containerd uses OCI-compliant runtimes and image formats. This standards-based design means images built by Docker run under Podman, CRI-O, and Kubernetes without modification .
a. The Docker CLI and REST API
The Docker CLI is the docker binary you invoke from your terminal. It is a thin client: it contains no container-running logic, no image-building logic, and no runtime management. Its only job is to parse your command-line arguments, translate them into REST API calls, and send those calls to the Docker daemon over a Unix socket or TCP connection .
When you type docker run nginx, the CLI constructs a JSON request body and sends a POST request to the daemon’s /containers/create endpoint, followed by a /containers/start request . The daemon’s response determines what the CLI prints. This separation means the CLI can run on your laptop while the daemon runs on a remote server in the cloud. You configure the connection with the DOCKER_HOST environment variable or the -H flag .
The REST API is the contract between the CLI and the daemon. It exposes endpoints for every Docker operation: listing containers, creating images, inspecting networks, managing volumes. The CLI is one client of this API; tools like Portainer, CI systems, and custom automation are others . The API also enables the daemon to be controlled programmatically, which is essential for orchestration systems and continuous deployment pipelines.
b. The Docker daemon (dockerd)
The Docker daemon, dockerd, is the long-running background service that does the real work at the Docker platform level . It listens for API requests on /var/run/docker.sock by default and manages Docker-specific concepts: networks, volumes, image tagging, and docker build .
When a docker run request arrives, dockerd parses and validates it. It checks whether the requested image exists locally. If not, it pulls the image from the configured registry. Once the image is available, dockerd prepares the container configuration and delegates the actual container creation to containerd . From this point forward, dockerd is not directly involved in the container’s lifecycle. It can be restarted without affecting running containers because containerd and the shims keep them alive.
dockerd also exposes the Docker REST API that the CLI and other tools consume. It handles authentication and authorization through plugins if configured. It manages the Docker data directory, which by default is /var/lib/docker, and it controls global settings like default cgroup parents and log formats .
c. containerd: the container lifecycle manager
containerd is a separate daemon that manages the complete container lifecycle: image transfer and storage, container execution and supervision, and low-level storage . It is a CNCF graduated project, meaning it has reached the highest maturity level in the foundation’s project lifecycle .
When dockerd hands off a container creation request, containerd does the following:
- Image management. It pulls the image from the registry if not already present, unpacks the image layers into a filesystem bundle, and manages the content-addressable storage of image data .
- Bundle preparation. It converts the image into an OCI-compliant filesystem bundle — a directory containing the root filesystem and a
config.jsonfile that describes how to run the container . - Runtime invocation. It selects the appropriate low-level runtime (runc by default) and spawns a shim process, which in turn invokes the runtime to create the container .
- Lifecycle supervision. It monitors the container’s state, handles start, stop, pause, and delete operations, and reports status back to dockerd .
containerd also exposes a gRPC API that dockerd uses. This same API is what Kubernetes talks to directly through the Container Runtime Interface, which is why Kubernetes can operate without Docker .
d. runc: the low-level OCI runtime
runc is the lowest layer of the stack and the component that actually creates the container. It is a small CLI tool that implements the OCI Runtime Specification . It receives a filesystem bundle and does the kernel-level work to turn that bundle into a running process.
runc’s operations are direct calls to Linux kernel primitives :
clone()with namespace flags. This creates the new process with its own PID, network, mount, UTS, IPC, and user namespaces. The process inside the new namespaces sees only the isolated view that the namespaces provide.- cgroup configuration. It writes to the cgroup filesystem to set CPU, memory, and I/O limits for the container’s process tree.
pivot_root(). This changes the root filesystem for the container process, replacing the host’s root with the container’s root filesystem from the bundle. After this call, the process cannot see the host’s filesystem.- Process execution. It
execs the container’s entry point — the command specified in the image’s configuration.
A crucial detail: runc exits after starting the container. It does not stay running to supervise the process. That responsibility falls to the shim . This design makes runc a short-lived tool that does one thing and exits, while a persistent process (the shim) handles ongoing supervision.
e. containerd-shim: decoupling container lifetimes
The containerd-shim is a small process that sits between containerd and the container’s parent process. One shim runs per container. Its existence solves a specific problem: if containerd managed container processes directly, restarting containerd would kill all containers. The shim acts as the container’s parent, independent of containerd .
When containerd creates a container, it spawns a shim. The shim calls runc to create the container, runc exits, and the shim remains as the container’s parent process. The shim reports the container’s exit status back to containerd, keeps the standard input/output/error pipes open for docker logs and docker attach, and stays alive even if containerd or dockerd restarts .
This means you can upgrade the Docker daemon or containerd without stopping your running containers. The shims keep everything alive and reconnect when the management plane comes back online . The shim is the reason the claim “restarting Docker kills containers” is no longer true with modern Docker .
f. The complete request path
When you run docker run nginx, the following sequence occurs:
- The Docker CLI translates the command into REST API calls and sends them over
/var/run/docker.sockto dockerd. - dockerd validates the request, checks for the
nginximage locally, and pulls it from Docker Hub if missing. - dockerd hands container creation to containerd via gRPC.
- containerd prepares the OCI bundle from the image layers and spawns a containerd-shim.
- The shim invokes runc with the bundle path.
- runc creates the container process using
clone()with namespace flags, sets cgroup limits, performspivot_root(), andexecs the nginx entry point. - runc exits. The shim remains as the container’s parent, monitoring its state.
- containerd reports success back to dockerd, which reports success to the CLI.
From the user’s perspective, one command. Beneath the surface, four distinct processes coordinated through standardized interfaces .
Complete Example Session
# ============================================
# PART 1: VIEW THE FULL VERSION STACK
# ============================================
# docker version shows client and server components.
docker version
# Output includes:
# Client: Docker Engine - Community
# Version: 28.x.x
# ...
# Server: Docker Engine - Community
# Engine:
# Version: 28.x.x
# containerd:
# Version: v1.7.x
# runc:
# Version: 1.2.x
# ============================================
# PART 2: CHECK WHICH RUNTIME IS ACTIVE
# ============================================
# docker info shows the default runtime.
docker info | grep -i runtime
# Output: Runtimes: runc io.containerd.runc.v2
# Default Runtime: runc
# ============================================
# PART 3: LIST PROCESSES ON THE HOST
# ============================================
# See the daemon, containerd, and shims.
ps aux | grep -E 'dockerd|containerd|runc|shim' | grep -v grep
# Output typically includes:
# /usr/bin/dockerd
# /usr/bin/containerd
# /usr/bin/containerd-shim-runc-v2 ...
# ============================================
# PART 4: RUN A CONTAINER AND WATCH PROCESSES
# ============================================
# Start nginx in the background.
docker run -d --name web nginx
# Now check processes again:
ps aux | grep -E 'dockerd|containerd|shim|nginx' | grep -v grep
# You will see:
# dockerd
# containerd
# containerd-shim-runc-v2 (one per container)
# nginx: master process
# nginx: worker process
# ============================================
# PART 5: INSPECT THE SHIM
# ============================================
# The shim is the container's parent process.
docker inspect web | grep -i pid
# Find the shim by parent PID, then inspect:
ps -ef | grep containerd-shim | grep web
# ============================================
# PART 6: RESTART DOCKER DAEMON
# ============================================
# Containers survive dockerd restart thanks to shims.
sudo systemctl restart docker
# Wait a moment, then:
docker ps
# The web container is still running.
# dockerd reconnected to containerd and the shims.
# ============================================
# PART 7: RESTART CONTAINERD
# ============================================
# Even containerd can restart without killing containers.
sudo systemctl restart containerd
docker ps
# Container still running; shims kept it alive.
# ============================================
# PART 8: USE RUNCT DIRECTLY (advanced)
# ============================================
# runc can create containers without Docker or containerd.
# List containers known to runc:
sudo runc list
# This shows the same container, managed by the shim.
# ============================================
# PART 9: INSPECT THE OCI BUNDLE
# ============================================
# containerd stores OCI bundles on disk.
sudo ls /run/containerd/io.containerd.runtime.v2.task/moby/*/config.json
# The bundle contains the rootfs and config.json
# that runc reads to create the container.
# ============================================
# PART 10: UNDERSTAND THE LAYER SEPARATION
# ============================================
# Each layer can be replaced independently.
# Default runtime is runc:
docker info | grep "Default Runtime"
# Switch to an alternative runtime:
docker run --runtime io.containerd.runsc.v1 hello-world
# This uses gVisor's runsc instead of runc
# without changing dockerd or containerd.
These ten parts reveal the architecture in action: the version command shows the layered components, process listing reveals the daemon/containerd/shim hierarchy, daemon and containerd restarts demonstrate the shim’s resilience role, and direct runc inspection shows the OCI bundle at the bottom of the stack.
Quick Reference
Docker Engine Components
| Component | Role | Process Lifetime |
|---|---|---|
docker (CLI) | Sends REST API calls to daemon | Per command |
dockerd | Docker platform management, API server | Long-running daemon |
containerd | Container lifecycle, image management | Long-running daemon |
containerd-shim | Container parent, survives daemon restarts | Per container |
runc | Creates container via kernel primitives | Exits after creation |
Standards and Specifications
| Standard | Purpose |
|---|---|
| OCI Runtime Spec | Defines how to run a container bundle |
| OCI Image Spec | Defines image format and distribution |
| CRI | Kubernetes’ interface to container runtimes |
| gRPC | Protocol between dockerd and containerd |
Key Interfaces
| Interface | Between | Protocol |
|---|---|---|
| Docker API | CLI and dockerd | REST over Unix socket/TCP |
| containerd API | dockerd and containerd | gRPC over Unix socket |
| Shim API | containerd and shims | gRPC over Unix socket |
| OCI Bundle | containerd and runc | Filesystem + config.json |
Runtime Alternatives
| Runtime | Type | Use Case |
|---|---|---|
runc | OCI reference | Default, general purpose |
youki | runc drop-in | Faster, lower memory |
runsc (gVisor) | Shim-based | Stronger isolation |
kata | Shim-based | VM-level isolation |
wasmtime | Shim-based | WebAssembly workloads |
Best Practices
✅ Do This:
docker version # Inspect full component stack
docker info | grep Runtime # Check active runtime
sudo systemctl restart docker # Safe; containers survive
docker run --runtime io.containerd.runsc.v1 image # Use alternative runtimes
ps aux | grep containerd-shim # Verify shim per container
sudo runc list # Inspect runtime-level containers
❌ Don’t Do This:
docker version # ❌ Ignoring server section
kill -9 $(pgrep containerd) # ❌ Force-kill breaks shim communication
rm -rf /run/containerd # ❌ Deletes runtime state, orphans containers
docker run --runtime runc # ❌ Unnecessary; runc is default
sudo dockerd # ❌ Manually starting daemon conflicts with service manager
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| “Cannot connect to Docker daemon” | dockerd not running | sudo systemctl start docker |
| Containers die on daemon restart | Old Docker version without shim | Update to modern Docker; shims preserve containers |
| Runtime not found | Alternative runtime not registered | Add runtime configuration to /etc/docker/daemon.json |
containerd socket conflict | Multiple containerd instances | Check /run/containerd/containerd.sock |
| Shim processes accumulate | Orphaned containers | docker system prune removes stopped containers and shims |
runc list empty | Wrong namespace or socket | Use ctr -n moby containers list for Docker’s namespace |
Real-World Examples
1. Checking Component Versions
docker version --format '{{.Server.Version}}'
docker version --format '{{.Server.Components}}'
2. Viewing containerd Namespace
sudo ctr -n moby containers list
3. Inspecting a Container’s Shim
ps -ef | grep containerd-shim | grep $(docker inspect -f '{{.Id}}' web | cut -c1-12)
4. Using gVisor Runtime
docker run --runtime io.containerd.runsc.v1 hello-world
5. Configuring Alternative Runtime
{
"runtimes": {
"gvisor": {
"runtimeType": "io.containerd.runsc.v1"
}
}
}
6. Finding the OCI Bundle
sudo find /run/containerd -name config.json -path '*moby*'
7. Checking Runtime Features
sudo runc features
8. Restarting the Stack Safely
sudo systemctl restart docker
docker ps # containers survive
9. Comparing Runtimes
docker info --format '{{json .Runtimes}}'
10. Directly Listing runc Containers
sudo runc list --root /run/containerd/runc/moby
Visual
The Docker Engine Stack
┌──────────────────────────────────────────────────────────────┐
│ DOCKER ENGINE ARCHITECTURE │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ docker CLI │ │
│ │ Thin client. Translates commands to REST API calls. │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ │ REST API (Unix socket / TCP) │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ dockerd │ │
│ │ Docker platform daemon. Manages networks, volumes, │ │
│ │ images, build. Exposes Docker REST API. │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ │ gRPC (containerd.sock) │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ containerd │ │
│ │ Container lifecycle, image distribution, storage. │ │
│ │ CNCF graduated project. CRI for Kubernetes. │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ │ spawns shim, invokes runc │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ containerd-shim (one per container) │ │
│ │ Container parent. Survives daemon restarts. │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ │ exec runc │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ runc │ │
│ │ OCI runtime. Creates container via kernel calls. │ │
│ │ Exits after container is created. │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ │ clone(), pivot_root(), cgroups │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Linux Kernel │ │
│ │ Namespaces + cgroups. The actual isolation primitives.│ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Container Process (e.g., nginx) │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
The Request Path for docker run
┌──────────────────────────────────────────────────────────────┐
│ WHAT HAPPENS WHEN YOU TYPE docker run nginx │
│ │
│ 1. CLI ──REST──▶ dockerd │
│ POST /containers/create, POST /containers/start │
│ │
│ 2. dockerd checks image locally. │
│ If missing, pull from registry. │
│ │
│ 3. dockerd ──gRPC──▶ containerd │
│ "Create container from image" │
│ │
│ 4. containerd prepares OCI bundle from image layers. │
│ │
│ 5. containerd spawns containerd-shim. │
│ │
│ 6. shim ──exec──▶ runc bundle_path │
│ │
│ 7. runc: │
│ ├── clone() with namespace flags → new process │
│ ├── configure cgroups → resource limits │
│ ├── pivot_root() → container filesystem │
│ └── exec(nginx) → container is running │
│ │
│ 8. runc exits. shim remains as parent. │
│ │
│ 9. containerd reports success to dockerd. │
│ │
│ 10. dockerd reports success to CLI. │
│ │
│ User sees: "container started" │
└──────────────────────────────────────────────────────────────┘
The Role of the Shim
┌──────────────────────────────────────────────────────────────┐
│ WHY THE SHIM EXISTS: DAEMONLESS CONTAINERS │
│ │
│ WITHOUT SHIM: │
│ dockerd ──▶ containerd ──▶ container process │
│ │
│ If containerd restarts: │
│ dockerd ──X──▶ (containerd) ──X──▶ container dies │
│ │
│ ───────────────────────────────────────── │
│ │
│ WITH SHIM: │
│ dockerd ──▶ containerd ──▶ shim ──▶ container process │
│ (exits) │
│ │
│ If containerd restarts: │
│ dockerd ──▶ (containerd) │
│ │ │
│ └──── reconnects ────▶ shim ──▶ container │
│ │
│ The shim keeps the container running and reconnects │
│ when the management plane returns. │
│ │
│ This enables: │
│ - Daemon upgrades without downtime │
│ - containerd restarts without killing containers │
│ - Live migration of management plane │
└──────────────────────────────────────────────────────────────┘
OCI Standards Hold the Stack Together
┌──────────────────────────────────────────────────────────────┐
│ OPEN CONTAINER INITIATIVE SPECIFICATIONS │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ OCI Image Specification │ │
│ │ - How images are formatted │ │
│ │ - How layers are stored and distributed │ │
│ │ - Used by: containerd, Docker, Podman, CRI-O │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ OCI Runtime Specification │ │
│ │ - How to run a filesystem bundle │ │
│ │ - Lifecycle operations (create, start, delete) │ │
│ │ - Configuration format (config.json) │ │
│ │ - Reference implementation: runc │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ OCI-Compliant Runtimes │ │
│ │ - runc (reference) │ │
│ │ - crun │ │
│ │ - youki │ │
│ │ - runsc (gVisor) │ │
│ │ - kata-runtime │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Any OCI image runs on any OCI runtime. This is why │
│ Docker images run on Podman and Kubernetes without change. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Docker CLI | Thin client; sends REST API calls to dockerd |
| dockerd | Docker platform daemon; networks, volumes, build, API |
| containerd | Container lifecycle, image management; CNCF project |
| containerd-shim | Per-container parent; survives daemon restarts |
| runc | OCI runtime; creates container via kernel calls |
| Docker API | REST over /var/run/docker.sock |
| containerd API | gRPC over containerd socket |
| OCI Runtime Spec | Defines container execution; runc is reference |
| OCI Image Spec | Defines image format and distribution |
| CRI | Kubernetes interface to containerd |
| Default runtime | runc |
| Alternative runtimes | gVisor (runsc), Kata, youki, Wasmtime |
Key takeaways:
- Docker is a layered stack, not a single program. The CLI, daemon, containerd, shims, and runc are separate processes with distinct responsibilities.
- The CLI is a thin client. It contains no container logic; it translates commands to REST API calls over a Unix socket or TCP .
- dockerd handles Docker-specific concerns. Networking, volumes, image building, and the Docker API are its domain. It delegates container creation to containerd .
- containerd manages the container lifecycle. It pulls images, prepares OCI bundles, spawns shims, and supervises containers. Kubernetes talks to it directly through CRI .
- runc is the OCI runtime. It creates the container process using
clone(), cgroups, andpivot_root(), then exits. It does not supervise the container . - The containerd-shim decouples container lifetimes. It stays alive when containerd or dockerd restarts, keeping containers running and reconnecting to the management plane .
- OCI standards enable interoperability. The Runtime and Image Specifications allow Docker images to run on any OCI-compliant platform, from Podman to Kubernetes .
Remember: Docker’s architecture is the result of a deliberate modularization effort that extracted container runtime functionality from a monolithic daemon into separate, reusable components. The CLI talks to dockerd, dockerd talks to containerd, containerd uses runc, and a shim keeps it all resilient. Each layer has a clear responsibility, and each interface is standardized. Understanding this stack explains why Kubernetes could drop Docker, why alternative runtimes can replace runc, and why restarting the Docker daemon no longer kills your containers. The architecture is the answer to the question “what is Docker, really?” — not a single binary, but a coordinated set of components held together by open standards.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!