| |

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:

  1. 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 .
  2. Bundle preparation. It converts the image into an OCI-compliant filesystem bundle — a directory containing the root filesystem and a config.json file that describes how to run the container .
  3. 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 .
  4. 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:

  1. The Docker CLI translates the command into REST API calls and sends them over /var/run/docker.sock to dockerd.
  2. dockerd validates the request, checks for the nginx image locally, and pulls it from Docker Hub if missing.
  3. dockerd hands container creation to containerd via gRPC.
  4. containerd prepares the OCI bundle from the image layers and spawns a containerd-shim.
  5. The shim invokes runc with the bundle path.
  6. runc creates the container process using clone() with namespace flags, sets cgroup limits, performs pivot_root(), and execs the nginx entry point.
  7. runc exits. The shim remains as the container’s parent, monitoring its state.
  8. 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

ComponentRoleProcess Lifetime
docker (CLI)Sends REST API calls to daemonPer command
dockerdDocker platform management, API serverLong-running daemon
containerdContainer lifecycle, image managementLong-running daemon
containerd-shimContainer parent, survives daemon restartsPer container
runcCreates container via kernel primitivesExits after creation

Standards and Specifications

StandardPurpose
OCI Runtime SpecDefines how to run a container bundle
OCI Image SpecDefines image format and distribution
CRIKubernetes’ interface to container runtimes
gRPCProtocol between dockerd and containerd

Key Interfaces

InterfaceBetweenProtocol
Docker APICLI and dockerdREST over Unix socket/TCP
containerd APIdockerd and containerdgRPC over Unix socket
Shim APIcontainerd and shimsgRPC over Unix socket
OCI Bundlecontainerd and runcFilesystem + config.json

Runtime Alternatives

RuntimeTypeUse Case
runcOCI referenceDefault, general purpose
youkirunc drop-inFaster, lower memory
runsc (gVisor)Shim-basedStronger isolation
kataShim-basedVM-level isolation
wasmtimeShim-basedWebAssembly 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

PitfallWhy It HappensFix
“Cannot connect to Docker daemon”dockerd not runningsudo systemctl start docker
Containers die on daemon restartOld Docker version without shimUpdate to modern Docker; shims preserve containers
Runtime not foundAlternative runtime not registeredAdd runtime configuration to /etc/docker/daemon.json
containerd socket conflictMultiple containerd instancesCheck /run/containerd/containerd.sock
Shim processes accumulateOrphaned containersdocker system prune removes stopped containers and shims
runc list emptyWrong namespace or socketUse 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

ItemValue
Docker CLIThin client; sends REST API calls to dockerd
dockerdDocker platform daemon; networks, volumes, build, API
containerdContainer lifecycle, image management; CNCF project
containerd-shimPer-container parent; survives daemon restarts
runcOCI runtime; creates container via kernel calls
Docker APIREST over /var/run/docker.sock
containerd APIgRPC over containerd socket
OCI Runtime SpecDefines container execution; runc is reference
OCI Image SpecDefines image format and distribution
CRIKubernetes interface to containerd
Default runtimerunc
Alternative runtimesgVisor (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, and pivot_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!