Docker & ContainersXXXII · Docker Internalsdockerd
dockerd internals — what the daemon actually does
What you'll learn
- Trace a request through the daemon
- Identify the daemon's subsystems
- Diagnose daemon-level failures
- Locate the daemon state on disk and know what each directory holds
- Decide whether a symptom belongs to dockerd, containerd, or the container
Prerequisites
Verified against Docker Engine 29.x · Docker Engine 28.x · Docker Compose 2.x · containerd 2.x · runc 1.2.x · BuildKit 0.20+ · Linux kernel 5.15+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-12
dockerd is a single binary, but it orchestrates many subsystems. Understanding the subsystems helps you diagnose daemon-level failures.
The most useful single fact about dockerd is negative: it is not in the
data path of a running container. Once a container is running, its
process has no relationship to dockerd at all — not as a parent, not as a
supervisor, not as a proxy for its I/O beyond the log stream. That is why
systemctl restart docker sometimes fixes everything, sometimes fixes
nothing, and sometimes causes an outage, and knowing which is which is
the practical payoff of this lesson.
The subsystems
flowchart TB
Client[CLI]
subgraph dockerd
REST["REST API<br/>unix socket, optionally TCP"]
Dispatcher[Dispatcher]
ImageMgr[Image manager]
VolumeMgr[Volume manager]
NetworkMgr["Network manager<br/>libnetwork"]
ExecMgr[Container exec manager]
ContainerdClient[containerd client]
end
Client -->|REST| REST
REST --> Dispatcher
Dispatcher --> ImageMgr
Dispatcher --> VolumeMgr
Dispatcher --> NetworkMgr
Dispatcher --> ExecMgr
Dispatcher --> ContainerdClient
ContainerdClient --> containerd
The dispatcher routes API calls to the right subsystem. Each subsystem owns its own data (image DB, volume DB, network DB).
The process tree, which is the real architecture
# The whole hierarchy
pstree -pas $(pgrep -x dockerd | head -1) | head -20
# Every layer at once
ps -eo pid,ppid,comm,args --forest \
| grep -E 'dockerd|containerd|shim|nginx' | grep -v grep
# The crucial relationship: who is the parent of a container's PID 1?
CONTAINER=web
PID=$(docker inspect --format '{{.State.Pid}}' "$CONTAINER")
ps -o pid,ppid,comm -p "$PID"
ps -o pid,comm -p "$(ps -o ppid= -p "$PID" | tr -d ' ')"Illustrative output:
PID PPID COMMAND
14822 9134 nginx
9134 1 containerd-shim
The container’s PID 1 (14822 on the host, 1 inside its namespace) has
containerd-shim-runc-v2 as its parent — and that shim’s own parent is
1, host init. Not dockerd. Not containerd. The shim was deliberately
re-parented to init so that neither daemon is in the container’s ancestry.
That single fact determines everything about daemon restarts.
What the daemon owns on disk
DATA_ROOT=$(docker info --format '{{.DockerRootDir}}')
sudo ls -1 "$DATA_ROOT"
# Per-container runtime state and the json-file log
sudo ls -1 "$DATA_ROOT"/containers | head
# Image and layer metadata (classic graphdriver image store)
sudo ls -1 "$DATA_ROOT"/image/overlay2/
# Named volumes
sudo ls -1 "$DATA_ROOT"/volumes | head
# Ephemeral execution state, re-derived on daemon start
sudo ls -1 /var/run/docker| Path | Holds | Survives reboot |
|---|---|---|
<data-root>/containers/<id>/ | Container config, hostconfig, the -json.log file | yes |
<data-root>/image/<driver>/repositories.json | Tag to image-ID mapping, as plain JSON | yes |
<data-root>/image/<driver>/imagedb/ | Image configs by digest | yes |
<data-root>/image/<driver>/layerdb/ | Layer chain metadata | yes |
<data-root>/overlay2/ | The actual layer contents | yes |
<data-root>/volumes/ | Named volume data | yes |
<data-root>/network/files/local-kv.db | libnetwork’s BoltDB store | yes |
/var/run/docker/ (exec root) | Runtime state, netns handles, sockets | no |
The split matters during recovery. The exec root is scratch: it is re-derived on daemon start and losing it costs nothing. The data root is your images, your volumes and your container definitions, and there is no mechanism that rebuilds it.
Daemon configuration and the reload boundary
The daemon reads /etc/docker/daemon.json at startup — or
~/.config/docker/daemon.json in rootless mode. Some keys can be applied
without a restart; most cannot.
# Parse and validate without touching the running daemon
sudo dockerd --validate --config-file=/etc/docker/daemon.json
# SIGHUP reload for the keys that support it
sudo systemctl reload docker
# Confirm the daemon actually took the change
docker info --format 'debug={{.Debug}} live-restore={{.LiveRestoreEnabled}} driver={{.Driver}}'The reloadable set is documented and short: debug, labels,
live-restore, max-concurrent-downloads, max-concurrent-uploads,
max-download-attempts, default-runtime, runtimes,
authorization-plugin, insecure-registries, registry-mirrors,
shutdown-timeout, and features.
Everything else — data-root, storage-driver, bip, default-address- pools, iptables, userland-proxy, log-driver — needs a full restart,
which means it needs a maintenance window on a host running production
containers.
--validate is the step people skip, and skipping it is how a host ends
up with a daemon that will not start at 03:00 because of a trailing comma.
It parses the file and exits without touching anything.
Debug logging
# Option A: SIGHUP-reloadable via daemon.json
sudo tee /etc/docker/daemon.json >/dev/null <<'JSON'
{ "debug": true }
JSON
sudo dockerd --validate --config-file=/etc/docker/daemon.json
sudo systemctl reload docker
docker info --format 'debug={{.Debug}}'
# Watch the daemon's own log while you reproduce
sudo journalctl -u docker.service -f
# Force a full stack trace of every daemon goroutine into the log.
# Use this when dockerd is hung rather than erroring.
sudo kill -s USR1 "$(pgrep -x dockerd | head -1)"
# Turn it back off when you are done; debug output is voluminous
sudo tee /etc/docker/daemon.json >/dev/null <<'JSON'
{ "debug": false }
JSON
sudo systemctl reload dockerThe SIGUSR1 stack dump is the tool for the specific case where
docker ps hangs and nothing is being logged. Docker’s troubleshooting
documentation describes it as forcing “a full stack trace of all threads
to be added to the daemon log”. The trace names the function every
goroutine is blocked in, which usually identifies the wedged subsystem —
a containerd call that never returned, a lock held by a stuck image pull
— without any guesswork.
Turn debug off afterwards. It is genuinely verbose, and on a busy host it becomes a disk-space problem of its own.
live-restore: the feature that exists because of the shim
Because the shims are not children of dockerd, dockerd can exit without
taking containers with it. By default it does not — it stops them on the
way out. live-restore changes that.
sudo tee /etc/docker/daemon.json >/dev/null <<'JSON'
{ "live-restore": true }
JSON
sudo dockerd --validate --config-file=/etc/docker/daemon.json
sudo systemctl reload docker
docker info --format 'live-restore={{.LiveRestoreEnabled}}'Docker’s documentation states the default plainly: “By default, when the Docker daemon terminates, it shuts down running containers.” With live-restore, containers “remain running if the daemon becomes unavailable”.
The limits are equally explicit and worth knowing before you rely on it:
- It “is only supported when installing patch releases (
YY.MM.x), not for major (YY.MM) daemon upgrades”. A major-version upgrade will stop your containers regardless. - It “only works to restore containers if the daemon options, such as
bridge IP addresses and graph driver, didn’t change”. Change
bipand restart, and you may need to stop the containers manually. - While the daemon is down, nothing is draining the containers’ log FIFOs. Docker warns that “running containers may fill up the FIFO log the daemon normally reads”, and “a full log blocks containers from logging more data” — with a 64K default buffer. A chatty container will block on its own stdout during an extended daemon outage, which looks like the application hanging.
That third limitation is the one that surprises people. live-restore keeps containers running; it does not keep them unaffected.
Diagnosing a daemon that will not start
# 1. What does systemd say, and what does the daemon say?
systemctl status docker.service --no-pager
sudo journalctl -u docker.service -n 50 --no-pager
# 2. Is the config valid? This is the most common cause.
sudo dockerd --validate --config-file=/etc/docker/daemon.json
# 3. Is the data root's filesystem present and writable?
DATA_ROOT=$(grep -o '"data-root"[^,]*' /etc/docker/daemon.json 2>/dev/null || echo default)
echo "configured data-root: $DATA_ROOT"
df -h /var/lib/docker
df -i /var/lib/docker
# 4. Is containerd healthy? dockerd depends on it.
systemctl status containerd.service --no-pager
sudo ctr versionStep 4 catches a failure mode that reads as a Docker problem and is not:
if containerd.service is down or its socket is missing, dockerd will
start and then fail its connection, logging something about the containerd
gRPC address. Restarting docker will not help; restarting containerd
will — and on a host with running containers, restarting containerd is
its own decision because the shims survive it but reconnection can be
imperfect.
Verification
#!/usr/bin/env bash
set -euo pipefail
# The unit being active is necessary, not sufficient
systemctl is-active --quiet docker.service \
|| { echo 'FAIL: unit not active' >&2; exit 1; }
# The API answers
docker info >/dev/null 2>&1 || { echo 'FAIL: API not responding' >&2; exit 1; }
# It can actually create and run something end to end
docker run --rm alpine:3.20 true \
|| { echo 'FAIL: cannot run a container' >&2; exit 1; }
# containerd is reachable from dockerd's point of view
docker info --format '{{.ContainerdCommit.ID}}' | grep -q . \
|| { echo 'FAIL: no containerd commit reported' >&2; exit 1; }
# Warnings the daemon is reporting about itself
docker info --format '{{range .Warnings}}WARN {{.}}
{{end}}'
echo OKsystemctl is-active alone is the check that lets a half-broken daemon
pass. A dockerd that has lost its containerd connection is active and
useless; the docker run line is what catches it.
Knowledge check
Knowledge check · 7 questions
Q1. `dockerd` listens for the CLI on:
Q2. What is the parent process of a running container's PID 1 on the host?
Q3. You change `"data-root"` in daemon.json and run `systemctl reload docker`. What happens?
Q4. Which are documented limitations of `live-restore`? Select all that apply.
Q5. dockerd is dead and containers are still serving traffic. Which actions are appropriate? Select all that apply.
Q6. A TCP Docker daemon without TLS is safe inside a private network.
Q7. A syntax error in daemon.json prevents the daemon from starting at all, rather than falling back to defaults.
Passing score: 75%. Answers are checked in this browser.