Docker & ContainersIII Β· Installation & Daemondaemon.json
daemon.json β every production-relevant key
What you'll learn
- Configure daemon.json for production hosts
- Explain each production-relevant key
- Separate the keys that reload on SIGHUP from the keys that need a full daemon restart
- Validate a configuration before it can stop the daemon from starting
- Recover a host whose daemon will not start because of a bad daemon.json
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
/etc/docker/daemon.json is the single source of truth for
dockerdβs behaviour. It is also the only file on a Docker host that can stop
the daemon from starting at all, and a daemon that will not start takes every
container with it. That asymmetry β cheap to edit, expensive to get wrong β is
what this lesson is really about.
The file does not exist by default on most installations. The daemon runs with built-in defaults until you create it.
A production-shaped daemon.json
"live-restore": true
"data-root": "/var/lib/docker"
"storage-driver": "overlay2"
"log-driver": "json-file"
"log-opts.max-size": "10m"
"log-opts.max-file": "3"
"userland-proxy": false
"no-new-privileges": true
"default-ulimits.nofile": { "Name": "nofile", "Hard": 65536, "Soft": 65536 }
"default-runtime": "runc"
"exec-opts": ["native.cgroupdriver=systemd"]
"metrics-addr": "127.0.0.1:9323"
"features": { "containerd-snapshotter": true }
"registry-mirrors": ["https://mirror.example.com"]
"hosts": ["unix:///var/run/docker.sock"]01
"live-restore"= trueKeep standalone containers running when dockerd restarts. Reloadable on SIGHUP, so you can turn it on without an outage.
Production: Enable on any host where container uptime matters. Incompatible with Swarm services.
02
"data-root"= "/var/lib/docker"Where images, layers, volumes and container metadata live. Change this to move Docker onto a dedicated filesystem.
Production: Give /var/lib/docker its own filesystem so a runaway image pull cannot fill the root partition.
03
"storage-driver"= "overlay2"Filesystem layering driver. Requires a full daemon restart and, in practice, a rebuild of the data root.
Production: overlay2 on modern Linux. Changing it later orphans every existing image and container.
04
"log-driver"= "json-file"Default log driver for containers created after the change. Not reloadable, and existing containers keep the driver they were created with.
Production: Switch to journald or local where centralised logging exists.
05
"log-opts.max-size"= "10m"Per-log-file size cap before rotation. json-file writes unbounded without it.
06
"log-opts.max-file"= "3"Number of rotated log files to keep.
Production: Budget the worst case: 200 containers x 10m x 3 files is roughly 6 GB before you count the active file.
07
"userland-proxy"= falseStop spawning a docker-proxy process per published port and let the kernel NAT rules do the forwarding.
Production: A real saving at high port counts, but it changes IPv6-to-IPv4 port mapping behaviour. Test before rolling out.
08
"no-new-privileges"= trueApplies the no_new_privs kernel bit to every container by default, so a setuid binary inside a container cannot raise privileges.
Production: Default-on unless a specific image genuinely needs setuid (ping on some bases, sudo in dev images).
09
"default-ulimits.nofile"= { "Name": "nofile", "Hard": 65536, "Soft": 65536 }Default file-descriptor limits for containers, so you are not setting --ulimit on every run.
10
"default-runtime"= "runc"Default OCI runtime. Reloadable on SIGHUP, together with the runtimes map.
Production: runc is correct unless a threat model justifies gVisor or Kata as an additional named runtime.
11
"exec-opts"= ["native.cgroupdriver=systemd"]Selects the cgroup driver. Only cgroupfs or systemd are accepted.
Production: Match whatever else on the host manages cgroups. Mixing drivers gives you two managers writing the same tree.
12
"metrics-addr"= "127.0.0.1:9323"Expose the daemon Prometheus endpoint. The documented example binds loopback.
Production: Never 0.0.0.0. The endpoint is unauthenticated and describes your whole host.
13
"features"= { "containerd-snapshotter": true }Feature gates. containerd-snapshotter selects the containerd image store, which is the default on fresh Engine 29.0+ installs and opt-in before that.
Production: Turning it on hides images created under overlay2 until you turn it back off. Plan the migration.
14
"registry-mirrors"= ["https://mirror.example.com"]Pull-through cache for Docker Hub. Reloadable on SIGHUP.
Production: A mirror is the cheapest defence against Hub rate limits and Hub outages.
15
"hosts"= ["unix:///var/run/docker.sock"]Where dockerd listens. Adding a tcp:// entry exposes an unauthenticated root-equivalent API unless TLS is configured.
Production: Leave it alone. See the warning below about the fd:// conflict.
$ journalctl -u docker --no-pager -n 3dockerd[4412]: unable to configure the Docker daemon with file /etc/docker/daemon.json: \
the following directives are specified both as a flag and in the configuration file: \
hosts: (from flag: [fd://], from file: [unix:///var/run/docker.sock tcp://0.0.0.0:2375])
systemd[1]: docker.service: Main process exited, code=exited, status=1/FAILURE
systemd[1]: docker.service: Failed with result 'exit-code'.Illustrative output
The fix is not to delete daemon.json. It is to stop passing the option twice:
add a systemd drop-in that clears ExecStart and re-issues it without -H,
so the socket list comes from exactly one place. That drop-in belongs in
configuration management, because the next package upgrade rewrites the unit
file and not the drop-in.
The reload boundary
βRestart the daemonβ is the advice everywhere, and it is usually right β but
not always necessary, and knowing the difference is worth real downtime.
systemctl reload docker runs ExecReload=/bin/kill -s HUP $MAINPID. The
daemon re-reads daemon.json on SIGHUP but only applies a fixed subset of
keys.
| Reloadable on SIGHUP | Requires a full daemon restart |
|---|---|
debug | log-driver, log-opts |
labels | storage-driver, storage-opts |
live-restore | data-root, exec-root |
max-concurrent-downloads | hosts, tls* |
max-concurrent-uploads | userland-proxy |
max-download-attempts | no-new-privileges |
default-runtime | exec-opts (cgroup driver) |
runtimes | bip, default-address-pools, mtu |
authorization-plugin | iptables, ip6tables, firewall-backend |
insecure-registries | metrics-addr |
registry-mirrors | userns-remap |
shutdown-timeout | default-ulimits, default-shm-size |
features | everything else |
Two consequences fall straight out of that table.
live-restore is reloadable. You can turn it on with a SIGHUP, no
container downtime, and only then schedule the restart that needs it. Turning
it on and immediately restarting to βmake it take effectβ is backwards.
log-driver is not reloadable. A systemctl reload docker after changing
the log driver silently changes nothing, which is exactly the shape of failure
that gets diagnosed as βthe log driver is brokenβ three weeks later.
Validating before you commit
dockerd --validate parses the file and exits non-zero if it is invalid,
without starting a daemon. It is safe to run on a host with a daemon already
running.
$ sudo dockerd --validate --config-file=/etc/docker/daemon.json; echo rc=$?configuration OK
rc=0Illustrative output
$ sudo dockerd --validate --config-file=/etc/docker/daemon.json; echo rc=$?unable to configure the Docker daemon with file /etc/docker/daemon.json: \
invalid character '}' looking for beginning of object key string
rc=1Illustrative output
A bare JSON syntax check catches less than --validate does β it will not
catch a wrong type or an unknown key β but it needs no privileges and belongs
in a pre-commit hook:
CONFIG=/etc/docker/daemon.json
python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$CONFIG" \
&& echo "JSON parses"Verification that can fail
systemctl restart docker exiting 0 tells you systemd started a process. It
does not tell you your key took effect. Read the value back:
$ docker info --format 'live-restore={{.LiveRestoreEnabled}} cgroup={{.CgroupDriver}} storage={{.Driver}} logging={{.LoggingDriver}}'live-restore=true cgroup=systemd storage=overlay2 logging=json-fileIllustrative output
For keys that docker info does not surface, ask the daemon what it parsed by
reading its own view of the file back through the API:
WANT=true
GOT=$(docker info --format '{{.LiveRestoreEnabled}}')
if [ "$GOT" != "$WANT" ]; then
echo "live-restore is $GOT, expected $WANT" >&2
exit 1
fi
echo "live-restore confirmed active"Undoing a change
Because daemon.json can prevent the daemon from starting, the rollback path
has to work when docker itself does not.
- Keep
/etc/docker/daemon.jsonin version control, or at minimum copy it todaemon.json.bakbefore every edit.cpis cheaper than a 40-minute outage. - On a daemon that will not start, read
journalctl -u docker -b --no-pager | head -20. The parse error is in the first few lines and names the offending construct. - Restore the previous file and run
sudo dockerd --validate --config-file=/etc/docker/daemon.jsonbefore you attempt another start. - Start the daemon with
sudo systemctl start dockerand confirm withdocker info, not withsystemctl is-activeβ systemd reports a daemon that started and is failing calls as active. - If the file itself is fine but the flag conflict above is the cause, inspect the effective unit with
systemctl cat dockerand look for a drop-in you did not write.
Common mistakes
Sanity workflow after every daemon.json change
- Back up the current file:
sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.bak. - Edit, then validate with
sudo dockerd --validate --config-file=/etc/docker/daemon.jsonand check the exit status. - Decide reload or restart by looking up every key you touched in the reload table above. When in doubt, restart.
- If a restart is needed, confirm
live-restoreis already active first β that is itself a reloadable change you can make in advance. - Apply:
sudo systemctl reload dockerorsudo systemctl restart docker. - Verify the daemon believes the new value, with
docker info --formaton the specific field. Do not accept a zero exit status as proof. - Confirm containers are still running with
docker ps, and that a workload actually serves traffic β not just that its state says running. - Tail
journalctl -u docker -ffor a few minutes to catch warnings the restart emitted. - Commit the change to version control with the ticket reference.
Knowledge check
Knowledge check Β· 6 questions
Q1. You change `"log-driver"` in daemon.json and run `systemctl reload docker`, which exits 0. What has happened?
Q2. Which of these can be applied with `systemctl reload docker`, with no container downtime? Select all that apply.
Q3. A daemon.json with a JSON syntax error can leave a host running normally for months and then fail to start the daemon at the next reboot.
Q4. You add `"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]` to daemon.json on a stock package install and restart. What happens?
Q5. What is the correct way to prove a daemon.json change took effect?
Q6. Which command parses daemon.json and reports errors without starting the daemon?
Passing score: 75%. Answers are checked in this browser.