Docker & ContainersIII Β· Installation & Daemonsystemd
systemd integration β managing dockerd as a unit
What you'll learn
- Read the docker.service unit and explain every non-obvious directive
- Explain why Delegate=yes and KillMode=process are load-bearing, not cosmetic
- Match the daemon cgroup driver to systemd, and say what breaks when it does not
- Stop the daemon completely, including the activation socket
- Recognise a start-rate-limited daemon and clear it
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
The Docker daemon, containerd, and every per-container shim are processes that systemd knows about. When dockerd βcrashesβ, systemd decides whether to restart it, how many times, how long to wait, and β crucially β whether to take your containers down with it.
Three directives in the packaged units do almost all of that work, and none of them is obvious from the name.
The docker.service unit
This is the upstream unit, verbatim from the Moby repository, minus a couple of comments:
[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network-online.target nss-lookup.target docker.socket firewalld.service containerd.service time-set.target
Wants=network-online.target containerd.service
Requires=docker.socket
StartLimitBurst=3
StartLimitIntervalSec=60
[Service]
Type=notify
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutStartSec=0
RestartSec=2
Restart=always
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
# set delegate yes so that systemd does not reset the cgroups of docker containers
Delegate=yes
# kill only the docker process, not all processes in the cgroup
KillMode=process
OOMScoreAdjust=-500
[Install]
WantedBy=multi-user.target
The obvious fields first:
Requires=docker.socketβ the daemon will not start without the activation socket, and stopping the socket stops the service too.Wants=containerd.serviceβ a weaker dependency: dockerd will start without containerd and then fail every container operation.ExecStart=... -H fd://β listen on the file descriptor systemd hands over, rather than opening a socket itself. This is why a"hosts"key indaemon.jsoncollides with the unit and stops the daemon from starting.ExecReload=/bin/kill -s HUP $MAINPIDβsystemctl reload dockeris literally a SIGHUP, and the daemon applies only its documented subset of reloadable keys.Type=notifywithTimeoutStartSec=0β the daemon tells systemd when it is ready, and systemd will wait indefinitely. A host with thousands of container records genuinely takes minutes to become ready, and a timeout would turn that into a restart loop.
The three that actually matter are the ones with comments on them.
$ sudo systemctl start dockerJob for docker.service failed.
See "systemctl status docker.service" and "journalctl -xeu docker.service" for details.
docker.service: Start request repeated too quickly.
docker.service: Failed with result 'exit-code'.Illustrative output
βStart request repeated too quicklyβ is not the root cause and contains no information about it β it is systemdβs rate limiter refusing to try again. Two things follow:
# The real failure is in the FIRST attempt this boot, not the last.
journalctl -u docker -b --no-pager | head -40
# Clear the rate limiter so the next start is actually attempted.
sudo systemctl reset-failed docker.serviceResetting without fixing the cause just spends another three attempts.
The cgroup driver has to match
docker info reports a cgroup driver: systemd or cgroupfs. These are the
only two values --exec-opt native.cgroupdriver= accepts. On a systemd host,
the answer should be systemd.
{
"exec-opts": ["native.cgroupdriver=systemd"]
}The reasoning is the same as the Delegate=yes argument, one level down.
systemd allocates a cgroup per unit and acts as the cgroup manager for the
host. If Docker writes the cgroup filesystem directly with cgroupfs while
systemd manages the same tree, the host has two cgroup managers with two views
of available and in-use resources. The upstream guidance on this is blunt:
nodes configured that way βbecome unstable under resource pressure.β
$ docker info --format '{{.CgroupDriver}} cgroup v{{.CgroupVersion}}'systemd cgroup v2Illustrative output
docker.socket β the activation mechanism
The socket unit is short:
[Unit]
Description=Docker Socket for the API
[Socket]
ListenStream=/run/docker.sock
SocketMode=0660
SocketUser=root
SocketGroup=docker
[Install]
WantedBy=sockets.target
systemd holds /run/docker.sock open even when dockerd is not running. When a
client connects, systemd starts docker.service and hands it the file
descriptor β which is what -H fd:// in ExecStart consumes. This is why
docker info can succeed on a host where nobody started the daemon.
Note SocketGroup=docker and SocketMode=0660. That is the entire access
control on the Docker API: membership of the docker group. The Docker
post-install documentation states it plainly β βThe docker group grants
root-level privileges to the user.β Adding somebody to docker is
administratively identical to giving them passwordless root, because the API
they gain can mount the host root filesystem into a container.
sudo systemctl stop docker.socket
sudo systemctl stop docker.service
# Only if you also need the runtime down. This kills every container.
# sudo systemctl stop containerd.service
systemctl is-active docker.socket docker.serviceFor a stop that survives a reboot, disable both:
sudo systemctl disable --now docker.socket docker.service
# Stronger: refuses to start even if something pulls it in as a dependency.
# sudo systemctl mask docker.socket docker.serviceEditing the unit correctly
Never edit /lib/systemd/system/docker.service in place. The next
apt upgrade docker-ce overwrites it, silently reverting your change at the
worst possible moment. Use a drop-in:
sudo systemctl edit docker.service
# writes /etc/systemd/system/docker.service.d/override.conf[Service]
# ExecStart is a list-valued directive. Without the empty assignment first,
# systemd appends and then refuses to start with "more than one ExecStart".
ExecStart=
ExecStart=/usr/bin/dockerd --containerd=/run/containerd/containerd.sock
Environment="HTTPS_PROXY=http://proxy.example.com:3128"
Environment="NO_PROXY=localhost,127.0.0.1,.example.com"
That specific override β dropping -H fd:// β is the supported way to move the
socket list into daemon.json without the flag conflict.
sudo systemctl daemon-reload
systemctl cat docker.service # base unit plus every drop-in, in order
systemctl show docker.service -p ExecStart --no-pagersystemctl cat is the one to reach for during an incident on a host you did not
build. It shows the base unit and every drop-in, which is the only way to see
an override somebody added two years ago.
Diagnosing systemd-related failures
| Symptom | Likely cause | First command |
|---|---|---|
Cannot connect to the Docker daemon | Daemon down, or you are not in the docker group | systemctl status docker; id -nG |
Start request repeated too quickly | Rate limiter tripped; real error is earlier in the boot | journalctl -u docker -b | head -40 |
| Daemon starts, every container command fails | containerd down or its socket path wrong | systemctl status containerd |
| Daemon βrestarts itselfβ after you stopped it | Socket activation, not Restart= | systemctl is-active docker.socket |
| Container memory limits silently reset | Missing Delegate=yes, after a daemon-reload | systemctl show docker -p Delegate |
Containers die on systemctl restart docker | KillMode is not process, or live-restore is off | systemctl show docker -p KillMode |
| Host unstable under memory pressure | cgroup driver mismatch | docker info --format '{{.CgroupDriver}}' |
docker.sock missing after boot | Socket unit disabled or masked | systemctl status docker.socket |
A safe daemon-restart procedure
- Confirm live restore is already on:
docker info --format "{{.LiveRestoreEnabled}}". If it is not, enable it withsystemctl reload dockerfirst β that costs nothing. - Classify the change. Networking, storage-driver or data-root changes are container-restarting; a daemon binary patch or a log-level change is not.
- Record the ground truth you will compare against:
docker inspect --format "{{.State.Pid}}" "$CONTAINER"for a representative container. - Validate config before you touch the service:
sudo dockerd --validate --config-file=/etc/docker/daemon.json. - Restart:
sudo systemctl restart docker. - Wait for readiness by polling
docker info, not by sleeping a fixed interval β a busy host takes longer than you think. - Verify the PID you recorded is unchanged, and that the application itself answers.
docker psshowing Up is not evidence. - Read
journalctl -u docker -b --since "-5 min"for warnings the restart emitted.
Knowledge check
Knowledge check Β· 6 questions
Q1. You run `sudo systemctl stop docker`, and thirty seconds later `dockerd` is running again. Why?
Q2. `systemctl start docker` returns "Start request repeated too quickly". What does that tell you about the root cause?
Q3. Which directives in docker.service are load-bearing for container survival and resource control, rather than cosmetic? Select all that apply.
Q4. On a systemd host, running the Docker daemon with the `cgroupfs` cgroup driver gives you two cgroup managers with two views of the available resources.
Q5. You need to add an HTTPS proxy to the daemon environment. Where does the change belong?
Q6. Adding a user to the `docker` group is a limited grant that lets them manage containers but not the host.
Passing score: 75%. Answers are checked in this browser.