CoursesDocker in depthEngine architecture

Engine architecture

dockerd, containerd, shim, and runc.

Intermediate12 min · lesson 5 of 30

Docker isn't one program. Type docker run and four separate processes get involved, each one handing the work down to the next. A restaurant runs on the same relay. You give your order to the waiter, the waiter clips a ticket up in the kitchen, the kitchen passes it to a line cook, and one specialist stands at the stove and actually cooks it. Docker splits the work the same way. The docker command you type is the waiter: a thin CLI (command-line interface, the program that reads what you type) that turns your order into a REST call (representational state transfer, the same plain web-style request a browser makes) and sends it over a socket, which is a special file two programs use to talk to each other on one machine. The kitchen is dockerd, the Docker daemon, a background program that owns images, networks and volumes. dockerd never starts a container itself. It hands that job to containerd, the line cook that owns the container lifecycle. And the specialist at the stove is runc, the low-level runtime that makes the real Linux system calls (namespaces to give the process its own private view of the system, cgroups to cap how much it can consume, pivot_root to swap in its own filesystem) that turn a plain process into your container. Once you can name those four, a surprising amount of Docker's behaviour stops being mysterious.

From your command to a running container
1docker CLI
your command, sent as a REST call over the socket
2dockerd
owns images, networks, volumes
3containerd
owns the container lifecycle, speaks CRI
4shim + runc
runc builds the process, then exits
runc builds the namespaces and cgroups, execs your process, then quits. The shim stays behind as the container's parent and holds a socket open to containerd, so containerd can still reach the container long after runc is gone.

Two of those names deserve a proper introduction. runc is an OCI runtime, where OCI stands for Open Container Initiative, the industry standard that spells out what a container bundle looks like on disk and how a runtime should start it. It works like a standard plug shape. Because runc fits that shape, containerd can pull it out and push in a different runtime such as gVisor or Kata Containers, and dockerd never notices the swap. containerd also speaks CRI, the Container Runtime Interface, which is the connection Kubernetes uses to talk to a runtime directly. That single fact is why a Kubernetes node needs containerd and has no use for dockerd, and you'll see it with your own eyes further down.

terminal
$ docker version
Client: Docker Engine - Community
Version: 27.3.1
API version: 1.47
Go version: go1.22.7
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 27.3.1
API version: 1.47 (minimum version 1.24)
containerd:
Version: 1.7.22
runc:
Version: 1.1.14
docker-init:
Version: 0.19.0

Read that top to bottom and the whole stack is laid out for you. The Client block is the docker command sitting on your laptop. The Server block is the machine actually running your containers, and it names every layer on its own line: Engine (that's dockerd), then containerd, then runc, then docker-init. That last one is tini, a very small startup program Docker can run as PID 1 (process ID 1, the first process inside the container) whose only job is to clean up orphaned child processes, the ones people call zombies. When you file a bug, or work out whether a CVE (Common Vulnerabilities and Exposures, the public catalogue of known security flaws) applies to your runtime, this is the exact output someone will ask you to paste.

Every layer is a process you can see

None of this stays a drawing. Each layer is a real process with a real number, so list them and you can point at the architecture. You get one dockerd, one containerd, and one containerd-shim for every running container. What you will not find is runc. It finished its work and exited the instant your process started, which is exactly why the shim exists. Something has to stay attached to the container once runc walks away.

terminal
$ ps -ef | grep -E 'dockerd|containerd|shim' | grep -v grep
root 1024 1 0 09:12 ? 00:00:31 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root 998 1 0 09:12 ? 00:00:44 /usr/bin/containerd
root 2087 1 0 09:20 ? 00:00:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 7f8e9d0a1b2c -address /run/containerd/containerd.sock
# the shim's parent process ID (PPID) is 1, not dockerd or containerd. The container process hangs off the shim, so it isn't a child of either daemon. runc is gone; it exited the moment the container started.

The same split tells you where to look when something goes wrong. Commands that hang, or come back with "cannot connect to the Docker daemon," point at dockerd, the top of the stack. A container that refuses to start with a runtime error about namespaces or cgroups points at runc, the bottom. Knowing which process owns which job turns a useless "Docker is broken" into one specific place to start looking.

Restart the daemon without dropping containers

Say you need to edit daemon.json and bounce dockerd, and the host is carrying production containers you cannot afford to lose. The architecture is on your side here. dockerd is not the parent of your containers. The shim is. Nothing in the process tree forces a container to stop when the daemon exits. Switch on live-restore and your containers stay up for the whole window that dockerd is missing, then get picked back up when it returns. Daemon maintenance stops being a workload outage.

/etc/docker/daemon.json
{
"live-restore": true
}
terminal
$ sudo systemctl restart docker
$ docker ps
CONTAINER ID IMAGE STATUS
7f8e9d0a1b2c nginx:1.27 Up 40 minutes
# still Up. The daemon bounced; the shim held the container the whole time.

The hosts with no dockerd at all

On a Kubernetes node you will often search the process list for a Docker daemon and come up empty. The kubelet, the agent that starts and stops pods on each node, talks straight to containerd through the CRI and skips dockerd, which for Kubernetes was only ever an extra hop in the chain. You can poke at that lower layer yourself with containerd's own client, ctr, or the friendlier nerdctl. Start a container with docker, then list it through ctr, and the layering turns concrete. docker is a convenience sitting on top of containerd, and containerd is what holds your workload.

terminal
$ sudo ctr -n moby containers ls
CONTAINER IMAGE RUNTIME
7f8e9d0a1b2c docker.io/nginx:1.27 io.containerd.runc.v2
# moby is the namespace docker uses. On a real k8s node the namespace is k8s.io, and ps shows no dockerd at all.
The Docker socket is a root login
dockerd runs as root and listens on /var/run/docker.sock. Anyone who can reach that socket can ask it to start a container that bind-mounts the host's entire root filesystem, then read or write any file on the box as root. Socket access is root access. There is no gentler way to say it. Don't hand it out casually, don't mount it into containers you don't trust, and never expose it over the network without TLS (Transport Layer Security, the encryption that puts the padlock in a browser). Running the engine rootless is the real fix, and it gets a full treatment in the advanced course.

Where to look when "Docker is broken"

Hold the path CLI → dockerd → containerd → shim → runc in your head and triage gets much faster. Socket errors from the CLI are a dockerd problem or a permissions problem. Containers that sit in created and never reach running usually die in runc, or in the OCI bundle setup right before it. Kubernetes nodes that never had dockerd installed still run the identical bottom half through containerd and CRI. Live-restore works because the shim parents the container process, and that same design is why socket access equals root on the host.

Fix the layer that is actually broken instead of restarting everything. A hung docker ps can be an API fault inside dockerd while every workload keeps humming along under its shim. One container exiting on its own is often runc or seccomp, nothing to do with the daemon at all. Write down which process owns images, networks and volumes, and which one builds the namespaces, so the person on call doesn't bounce the whole host over a single sick container.

A rich engine versus a bare CRI runtime is a genuine trade, and you pay for it in operations. docker compose and all the friendly commands come bundled with a root daemon and a socket that hands over the host. Rootless Docker and podman-style stacks shrink that blast radius and give up some of the convenience. Find out what your hosts actually run before you paste a dockerd-centric runbook onto a node that only has containerd.

When you change something at this layer, record the docker version output from before and after, the hostname you ran it on, and the one command that puts it back. Engine changes are quiet in a way container changes are not. Nobody notices a swapped default runtime or a flipped live-restore until the next daemon restart, which might land weeks later in the middle of an incident. A ticket that reads "set live-restore on web-03, confirmed with docker ps showing nginx:1.27 still Up after systemctl restart docker" can be replayed by anyone on the team. "Fixed the daemon" cannot.

Try this

Run these on a lab engine. Docker 24 or newer is fine. Read the sample output first so you know what a healthy answer looks like, before you find yourself leaning on these commands during an incident.

terminal
$ docker info --format 'server={{.ServerVersion}} cri={{.CRI}} cgroup={{.CgroupVersion}}'
server=27.3.1 cri= cgroup=2
$ docker version
Client: Docker Engine - Community
Version: 27.3.1
Server: Docker Engine - Community
Engine: 27.3.1
containerd: 1.7.22
runc: 1.1.14
$ ps -ef | grep -E 'dockerd|containerd-shim' | grep -v grep
root 1024 1 0 09:12 ? 00:00:31 /usr/bin/dockerd
root 2087 1 0 09:20 ? 00:00:00 /usr/bin/containerd-shim-runc-v2 …

Takeaway

Next time someone tells you Docker is down, ask which process. dockerd, containerd, the shim and runc each fail differently, and every one of them is visible in ps. Keep live-restore on so daemon maintenance never costs you a workload. Treat docker.sock as the root login it is. And never assume a host still runs a Docker daemon merely because it is running containers.

Quick check
01live-restore is enabled, and you run systemctl restart docker on a host where a container is showing Up. What happens to that container?
Incorrect — The container process hangs off the shim, not dockerd, so bouncing the daemon doesn't kill it.
Correct — The shim owns the container process, and live-restore lets it keep running while dockerd is away, then the daemon reattaches on start.
Incorrect — Nothing freezes it. runc already execed your process and left, and the shim keeps it running with no help from dockerd.
Incorrect — runc exits the moment the container process starts, so it isn't around to hold anything open. That's the shim's job.
02Four processes take part in a docker run. Which one makes the real Linux system calls (namespaces, cgroups, pivot_root) that turn a plain process into a container, and then exits?
Incorrect — dockerd holds images, networks and volumes, but it never starts a container itself. It hands that job down the chain.
Incorrect — The CLI only turns what you typed into a REST call. It makes no container system calls at all.
Incorrect — containerd runs the lifecycle but passes the kernel-level work down to the OCI runtime beneath it.
Correct — runc does the kernel-level setup and then exits, which is precisely why the shim has to stay behind.
03A teammate wants to bind-mount /var/run/docker.sock into a semi-trusted CI container so the job can build images. What makes that risky?
Correct — dockerd runs as root, so reaching its socket amounts to having root on the host.
Incorrect — The socket exposes the full Docker API, not a read-only view, and that API can create privileged containers.
Incorrect — Socket access has nothing to do with which PID the container gets. The real problem is root-level control of the host.
Incorrect — It exposes the entire daemon, far beyond logs. An attacker can mount the host's root filesystem.

Related