Skip to main content
RunBook Academy

Docker & ContainersIII Β· Installation & Daemonsystemd

systemd integration β€” managing dockerd as a unit

Intermediate⏱ ~26 mindockersystemctl

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

Not yet marked complete on this device.

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 in daemon.json collides with the unit and stops the daemon from starting.
  • ExecReload=/bin/kill -s HUP $MAINPID β€” systemctl reload docker is literally a SIGHUP, and the daemon applies only its documented subset of reloadable keys.
  • Type=notify with TimeoutStartSec=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.

Read-only / Saferate limited
$ sudo systemctl start docker
Job 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:

Read-only / Safeclear and diagnose
# 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.service

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

Configuration changecgroup driver
{
"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.”

Read-only / Safeverify
$ docker info --format '{{.CgroupDriver}} cgroup v{{.CgroupVersion}}'
systemd cgroup v2

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

Service impact possiblefull stop
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.service

For a stop that survives a reboot, disable both:

Configuration changedisable
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.service

Editing 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:

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

Configuration changeapply
sudo systemctl daemon-reload
systemctl cat docker.service        # base unit plus every drop-in, in order
systemctl show docker.service -p ExecStart --no-pager

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

SymptomLikely causeFirst command
Cannot connect to the Docker daemonDaemon down, or you are not in the docker groupsystemctl status docker; id -nG
Start request repeated too quicklyRate limiter tripped; real error is earlier in the bootjournalctl -u docker -b | head -40
Daemon starts, every container command failscontainerd down or its socket path wrongsystemctl status containerd
Daemon β€œrestarts itself” after you stopped itSocket activation, not Restart=systemctl is-active docker.socket
Container memory limits silently resetMissing Delegate=yes, after a daemon-reloadsystemctl show docker -p Delegate
Containers die on systemctl restart dockerKillMode is not process, or live-restore is offsystemctl show docker -p KillMode
Host unstable under memory pressurecgroup driver mismatchdocker info --format '{{.CgroupDriver}}'
docker.sock missing after bootSocket unit disabled or maskedsystemctl status docker.socket

A safe daemon-restart procedure

  1. Confirm live restore is already on: docker info --format "{{.LiveRestoreEnabled}}". If it is not, enable it with systemctl reload docker first β€” that costs nothing.
  2. Classify the change. Networking, storage-driver or data-root changes are container-restarting; a daemon binary patch or a log-level change is not.
  3. Record the ground truth you will compare against: docker inspect --format "{{.State.Pid}}" "$CONTAINER" for a representative container.
  4. Validate config before you touch the service: sudo dockerd --validate --config-file=/etc/docker/daemon.json.
  5. Restart: sudo systemctl restart docker.
  6. Wait for readiness by polling docker info, not by sleeping a fixed interval β€” a busy host takes longer than you think.
  7. Verify the PID you recorded is unchanged, and that the application itself answers. docker ps showing Up is not evidence.
  8. Read journalctl -u docker -b --since "-5 min" for warnings the restart emitted.

Knowledge check

Knowledge check Β· 6 questions

  1. Q1. You run `sudo systemctl stop docker`, and thirty seconds later `dockerd` is running again. Why?

  2. Q2. `systemctl start docker` returns "Start request repeated too quickly". What does that tell you about the root cause?

  3. Q3. Which directives in docker.service are load-bearing for container survival and resource control, rather than cosmetic? Select all that apply.

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

  5. Q5. You need to add an HTTPS proxy to the daemon environment. Where does the change belong?

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