Skip to main content
RunBook Academy

Docker & ContainersXXXII · Docker Internalsdockerd

dockerd internals — what the daemon actually does

Advanced⏱ ~30 mindocker

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

Not yet marked complete on this device.

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

Read-only / Safesee the tree
# 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

Read-only / Safethe data root
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
PathHoldsSurvives reboot
<data-root>/containers/<id>/Container config, hostconfig, the -json.log fileyes
<data-root>/image/<driver>/repositories.jsonTag to image-ID mapping, as plain JSONyes
<data-root>/image/<driver>/imagedb/Image configs by digestyes
<data-root>/image/<driver>/layerdb/Layer chain metadatayes
<data-root>/overlay2/The actual layer contentsyes
<data-root>/volumes/Named volume datayes
<data-root>/network/files/local-kv.dblibnetwork’s BoltDB storeyes
/var/run/docker/ (exec root)Runtime state, netns handles, socketsno

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.

Configuration changevalidate, then reload
# 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

Configuration changeturn on debug without a restart
# 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 docker

The 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.

Configuration changeenable live-restore
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 bip and 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

Read-only / Safedaemon start failure
# 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 version

Step 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

Read-only / Safeverify the daemon is genuinely healthy
#!/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 OK

systemctl 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

  1. Q1. `dockerd` listens for the CLI on:

  2. Q2. What is the parent process of a running container's PID 1 on the host?

  3. Q3. You change `"data-root"` in daemon.json and run `systemctl reload docker`. What happens?

  4. Q4. Which are documented limitations of `live-restore`? Select all that apply.

  5. Q5. dockerd is dead and containers are still serving traffic. Which actions are appropriate? Select all that apply.

  6. Q6. A TCP Docker daemon without TLS is safe inside a private network.

  7. 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.