When you install docker-ce you install five packages. They cooperate to give
you the docker command. Knowing what each one does is the difference between
“the daemon is broken” and “containerd is broken” — two very different
troubleshooting paths, run against two different sets of logs.
The packages
Package
Provides
Role
docker-ce
dockerd, docker-proxy, docker-init
The daemon: API, images, networks, volumes
docker-ce-cli
docker
The client. Talks to a daemon over a socket
containerd.io
containerd, containerd-shim-runc-v2, ctr, runc
The supervisor, the per-container shim, and the OCI runtime
docker-buildx-plugin
docker buildx
BuildKit integration
docker-compose-plugin
docker compose
Compose v2
Two things about that table are worth pausing on.
runc ships in containerd.io, not in docker-ce. So does the shim. The
Docker packages contain no container runtime at all. That is deliberate: a
Kubernetes node can install containerd.io alone and never have Docker on it.
The CLI is a separate package from the daemon. They are versioned
independently and can drift, which is why docker version reports them
separately.
The call chain, and the process that is not there
Read-only / Safeversion— All four components, in one call. The bottom three come from containerd.io.
$ docker version
Client: Docker Engine - Community
Version: 28.5.1
API version: 1.51
Go version: go1.24.8
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 28.5.1
API version: 1.51 (minimum version 1.24)
OS/Arch: linux/amd64
containerd:
Version: v2.1.5
runc:
Version: 1.2.6
docker-init:
Version: 0.19.0
Illustrative output
Now look at what is actually running on a host with containers on it:
Read-only / Safeprocesses— Two daemons, one shim per container, one proxy per published port. No runc.
The path itself is informative: io.containerd.runtime.v2.task is the shim’s
runtime plugin, and moby is the containerd namespace Docker uses. That
namespace is the reason for the next failure mode.
When the layers disagree
Each layer has its own client and its own log, and using the right one is most
of the diagnosis.
Read-only / Safelayer by layer— Ask each layer separately. Where the answers diverge is where the fault is.
# Layer 1: does the daemon answer at all?
docker info >/dev/null 2>&1 && echo "dockerd OK" || echo "dockerd DOWN"
journalctl -u docker -b --no-pager | tail -20
# Layer 2: does containerd answer, and does it see the same containers?
sudo ctr --namespace moby containers list
journalctl -u containerd -b --no-pager | tail -20
# Layer 3: are the tasks actually running?
sudo ctr --namespace moby tasks list
Symptom
Layer at fault
Evidence that confirms it
Cannot connect to the Docker daemon
dockerd, or the socket
systemctl status docker, journalctl -u docker -b
Daemon answers; every container operation errors
containerd, or its socket
systemctl status containerd; ctr --namespace moby containers list
Container fails to start with a permissions or mount error
runc, or the OCI config
.State.Error in docker inspect; the bundle config.json
Container exits 126 or 127
Not a layer fault at all — the image
docker inspect --format '{{json .Config}}'
docker ps empty but the application still serves
dockerd restarted and lost nothing; you are looking at live restore
ctr --namespace moby tasks list
Images disappeared after an upgrade
The image store switched
docker info --format '{{.Driver}}'
Read-only / Safecontainerd down— What a healthy daemon on top of a dead runtime looks like.
$ docker run --rm hello-world
docker: Error response from daemon: failed to create task for container: \
failed to dial "/run/containerd/containerd.sock": \
connection error: desc = "transport: Error while dialing: dial unix \
/run/containerd/containerd.sock: connect: connection refused"
Illustrative output
Note that docker info succeeds throughout this. The daemon is fine. Restarting
docker.service will not help and, on a host where containers are still running
under their shims, is a strictly worse move than starting containerd. Start
containerd first, then re-test; only restart dockerd if it fails to reconnect.
Why the split exists
Verification that can fail
Read-only / Safecomponent check— Assert all four components report a version and that client and server agree.
set -e
CLIENT=$(docker version --format '{{.Client.Version}}')
SERVER=$(docker version --format '{{.Server.Version}}')
if [ "${CLIENT%%.*}" != "${SERVER%%.*}" ]; then
echo "major version skew: client $CLIENT, server $SERVER" >&2
exit 1
fi
docker version --format 'engine={{.Server.Version}} containerd={{.Server.Containerd.Version}} runc={{.Server.Runc.Version}} init={{.Server.Init.Version}}'
# The runtime layer answers independently of the daemon.
sudo ctr --namespace moby containers list >/dev/null
echo "all four components reporting"
A client and server on different major versions is worth failing a check
over: the API negotiates down to the server’s version, so newer client
subcommands and --format fields quietly stop working rather than erroring.
Knowledge check
Knowledge check · 6 questions
Q1. On a healthy host running fifty containers, how many `runc` processes should `ps` show?
Q2. `docker info` succeeds, but every `docker run` fails with "failed to dial /run/containerd/containerd.sock: connection refused". What is the correct first action?
Q3. Which binaries come from the `containerd.io` package rather than from `docker-ce`? Select all that apply.
Q4. `ctr containers list` on a Docker host returns an empty list unless you pass `--namespace moby`.
Q5. Where would you look for the exact capability set and cgroup path the kernel was configured with for a running container?
Q6. Why does `systemctl restart docker` with live restore leave containers running?
Passing score: 75%. Answers are checked in this browser.