Skip to main content
RunBook Academy

Docker & ContainersIII Β· Installation & Daemondaemon.json

daemon.json β€” every production-relevant key

Intermediate⏱ ~30 mindocker

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

Not yet marked complete on this device.

/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

/etc/docker/daemon.jsonSensible defaults for a production Linux host. Every key below is documented in the dockerd reference.
"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"]
  1. 01"live-restore"= true

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

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

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

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

  5. 05"log-opts.max-size"= "10m"

    Per-log-file size cap before rotation. json-file writes unbounded without it.

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

  7. 07"userland-proxy"= false

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

  8. 08"no-new-privileges"= true

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

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

Read-only / Safethe fd:// conflict
$ journalctl -u docker --no-pager -n 3
dockerd[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 SIGHUPRequires a full daemon restart
debuglog-driver, log-opts
labelsstorage-driver, storage-opts
live-restoredata-root, exec-root
max-concurrent-downloadshosts, tls*
max-concurrent-uploadsuserland-proxy
max-download-attemptsno-new-privileges
default-runtimeexec-opts (cgroup driver)
runtimesbip, default-address-pools, mtu
authorization-pluginiptables, ip6tables, firewall-backend
insecure-registriesmetrics-addr
registry-mirrorsuserns-remap
shutdown-timeoutdefault-ulimits, default-shm-size
featureseverything 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.

Read-only / Safevalidate
$ sudo dockerd --validate --config-file=/etc/docker/daemon.json; echo rc=$?
configuration OK
rc=0

Illustrative output

Read-only / Safevalidate (failing)
$ 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=1

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

Read-only / Safesyntax only
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:

Read-only / Safeverify
$ docker info --format 'live-restore={{.LiveRestoreEnabled}} cgroup={{.CgroupDriver}} storage={{.Driver}} logging={{.LoggingDriver}}'
live-restore=true cgroup=systemd storage=overlay2 logging=json-file

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

Read-only / Safeassert
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.

  1. Keep /etc/docker/daemon.json in version control, or at minimum copy it to daemon.json.bak before every edit. cp is cheaper than a 40-minute outage.
  2. 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.
  3. Restore the previous file and run sudo dockerd --validate --config-file=/etc/docker/daemon.json before you attempt another start.
  4. Start the daemon with sudo systemctl start docker and confirm with docker info, not with systemctl is-active β€” systemd reports a daemon that started and is failing calls as active.
  5. If the file itself is fine but the flag conflict above is the cause, inspect the effective unit with systemctl cat docker and look for a drop-in you did not write.

Common mistakes

Sanity workflow after every daemon.json change

  1. Back up the current file: sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.bak.
  2. Edit, then validate with sudo dockerd --validate --config-file=/etc/docker/daemon.json and check the exit status.
  3. Decide reload or restart by looking up every key you touched in the reload table above. When in doubt, restart.
  4. If a restart is needed, confirm live-restore is already active first β€” that is itself a reloadable change you can make in advance.
  5. Apply: sudo systemctl reload docker or sudo systemctl restart docker.
  6. Verify the daemon believes the new value, with docker info --format on the specific field. Do not accept a zero exit status as proof.
  7. Confirm containers are still running with docker ps, and that a workload actually serves traffic β€” not just that its state says running.
  8. Tail journalctl -u docker -f for a few minutes to catch warnings the restart emitted.
  9. Commit the change to version control with the ticket reference.

Knowledge check

Knowledge check Β· 6 questions

  1. Q1. You change `"log-driver"` in daemon.json and run `systemctl reload docker`, which exits 0. What has happened?

  2. Q2. Which of these can be applied with `systemctl reload docker`, with no container downtime? Select all that apply.

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

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

  5. Q5. What is the correct way to prove a daemon.json change took effect?

  6. Q6. Which command parses daemon.json and reports errors without starting the daemon?

Passing score: 75%. Answers are checked in this browser.