Engine architecture
dockerd, containerd, shim, and runc.
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.
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.
$ docker versionClient: Docker Engine - CommunityVersion: 27.3.1API version: 1.47Go version: go1.22.7OS/Arch: linux/amd64Context: defaultServer: Docker Engine - CommunityEngine:Version: 27.3.1API version: 1.47 (minimum version 1.24)containerd:Version: 1.7.22runc:Version: 1.1.14docker-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.
$ ps -ef | grep -E 'dockerd|containerd|shim' | grep -v greproot 1024 1 0 09:12 ? 00:00:31 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sockroot 998 1 0 09:12 ? 00:00:44 /usr/bin/containerdroot 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.
{"live-restore": true}
$ sudo systemctl restart docker$ docker psCONTAINER ID IMAGE STATUS7f8e9d0a1b2c 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.
$ sudo ctr -n moby containers lsCONTAINER IMAGE RUNTIME7f8e9d0a1b2c 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.
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.
$ docker info --format 'server={{.ServerVersion}} cri={{.CRI}} cgroup={{.CgroupVersion}}'server=27.3.1 cri= cgroup=2$ docker versionClient: Docker Engine - CommunityVersion: 27.3.1Server: Docker Engine - CommunityEngine: 27.3.1containerd: 1.7.22runc: 1.1.14$ ps -ef | grep -E 'dockerd|containerd-shim' | grep -v greproot 1024 1 0 09:12 ? 00:00:31 /usr/bin/dockerdroot 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.