Configuring the daemon
daemon.json and engine defaults.
Most of the knobs you turn in Docker are bolted to one container. A memory cap here, a --log-opt there, a published port. daemon.json sits underneath all of that. It is the config file for dockerd (the Docker daemon, the background program that actually builds your images and runs your containers), and whatever you write in it becomes the default for every container on that host. One is a sticky note taped to a single door. The other is the building's house rules. Flip a container flag and one workload behaves differently. Edit daemon.json and the whole node changes.
The file lives at /etc/docker/daemon.json, and it does not exist until you create it. It is plain JSON (JavaScript Object Notation, the curly-brace format an API hands back), so the syntax is fussy. One trailing comma or one missing quote and the file is invalid. A small handful of keys covers almost everything you will ever want to tune: where images and containers get stored (data-root), the default log driver and how it rotates, the storage driver, the range of subnets Docker hands out when it builds networks, and whether running containers survive a restart of the daemon. Here is a config with sensible defaults for a real production node.
{"data-root": "/var/lib/docker","storage-driver": "overlay2","log-driver": "json-file","log-opts": { "max-size": "20m", "max-file": "5" },"default-address-pools": [{ "base": "10.200.0.0/16", "size": 24 }],"registry-mirrors": ["https://mirror.acme.internal"],"live-restore": true}
Every one of those keys is earning its place. storage-driver: overlay2 pins the union filesystem, the mechanism that stacks your image layers into a single container root. It is the right pick on any modern kernel, and naming it out loud stops a host from quietly sliding onto the slow vfs driver without anyone noticing. The log-opts block caps each container's logs at 20 MB spread over five files, so one chatty service cannot eat the disk on its own. default-address-pools tells the daemon which private ranges to lease when it builds networks, the way DHCP (Dynamic Host Configuration Protocol, the service that hands your laptop an address when you join a WiFi network) works from a pool somebody defined in advance. registry-mirrors sends Docker Hub pulls through a caching mirror, so a rate limit upstream does not stall your builds. And data-root is the patch of disk all of it sits on. Point it at a bigger or faster volume and every image, container, and layer moves with it.
Make it take effect without causing an outage
Nothing in the file does anything until dockerd reads it again, and that only happens on restart. Two things make the restart safe. live-restore: true keeps your running containers alive while the daemon bounces, so editing a config file is not an outage. And dockerd --validate parses the file without going anywhere near the live daemon, so a typo costs you nothing. Validate, restart, then ask the engine what it actually loaded. Keep a copy of the last working file somewhere as well, because if a bad edit stops dockerd from starting, the command-line interface (the docker command you type) has nothing left to talk to and every command errors out. Two limits on live-restore are worth carrying around in your head. It will not hold containers through a full host reboot, and it does not run while the node is in Swarm mode (Docker's built-in clustering). It is built for daemon restarts, which is exactly what a config edit needs.
$ sudo dockerd --validate --config-file /etc/docker/daemon.jsonconfiguration OK$ sudo systemctl restart docker$ docker info -f 'Storage: {{.Driver}} | Logging: {{.LoggingDriver}} | Root: {{.DockerRootDir}}'Storage: overlay2 | Logging: json-file | Root: /var/lib/docker
Prove the defaults really landed
docker info tells you what the daemon believes about itself, which is a fine start. Behaviour is better proof. Start a container with no logging flags at all, then inspect it. The rotation limits from daemon.json are already sitting on it, because the engine stamped its own default onto the container without being asked.
$ docker run -d --name web nginx:1.27c4e1b9f0a2d3f6a1b8c7d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0$ docker inspect -f '{{.HostConfig.LogConfig}}' web{json-file map[max-file:5 max-size:20m]}
The address pool deserves the same treatment, because it is the setting most likely to save you from an outage nobody can explain. Set it once at the daemon level and you stop hand-picking subnets network by network. The engine allocates from a range you already trust. Create two networks and look at where they land. Both come out of the 10.200.0.0/16 block you defined, one /24 at a time, instead of Docker's built-in 172.x ranges.
$ docker network create net-a7f3a9c2e1b8d4f60a5c7e9203d6b8f014e2a9c7d5b0f3a86c1d4e70b92a6f358$ docker network create net-b4c8e1a096f2b7d53b0a4c6e812f9d3a78e5c0b46a37f1d920d6b4e8c5f1a2c70$ docker network inspect net-a -f '{{(index .IPAM.Config 0).Subnet}}'10.200.1.0/24$ docker network inspect net-b -f '{{(index .IPAM.Config 0).Subnet}}'10.200.2.0/24
One file, every container
daemon.json sets the defaults for the whole host: log driver and rotation, where data-root points, the address pools bridge networks draw from, userland-proxy, live-restore, and feature switches like BuildKit. Edit it carefully, because a single JSON typo stops dockerd from starting. Try the change on a canary host, keep a backup of the last good file, and prefer small diffs to a rewrite. Container-level flags still override many of these defaults, but a forgotten host default is usually the answer to "why is every container logging to json-file with no rotation?"
A few keys are security changes wearing operations clothes, and they deserve a named reviewer: a daemon listening on tcp:// without TLS (Transport Layer Security, the encryption behind HTTPS), anything sitting in insecure-registries, and experimental flags. Central defaults buy you consistency across every node; per-service flags buy you flexibility on one. Pick on purpose. Keep the node's daemon.json in the same repository as your Packer or Ansible roles, so drift cannot hide in a file someone hand-edited on a single manager.
Restart dockerd inside a maintenance window unless live-restore is on and you have watched containers reconnect with your own eyes. Tail journalctl -u docker for config parse errors before you walk away from the box. If data-root is moving, that gets its own plan for disk space and permissions, because a half-copied graph directory turns into a very long night.
Write the change down while you are making it: the host you ran it on, the daemon.json before and after, and the one command that puts the old file back. The configuration OK line from dockerd --validate and the docker info output you saw afterwards belong in the ticket too, because they are what the next person compares against when the node starts behaving oddly. If a teammate cannot replay your edit from the ticket alone, it is not finished yet.
Try this
Run these on a lab engine (Docker 24+ is fine), not on a host anyone depends on. Read the sample output first, so you know what a healthy result looks like before you trust the commands somewhere that matters.
$ sudo tee /etc/docker/daemon.json <<'EOF'{"log-driver": "json-file","log-opts": { "max-size": "10m", "max-file": "3" },"live-restore": true}EOF$ sudo systemctl restart docker$ docker info --format 'logging={{.LoggingDriver}} liveRestore={{.LiveRestoreEnabled}}'logging=json-file liveRestore=true# STATUS: daemon accepted config; containers stayed Up with live-restore
Takeaway
Keep daemon.json in version control alongside the host build, validate it before every restart, and choose your log rotation, live-restore and default-address-pools deliberately instead of inheriting whatever Docker ships. When a request lands for tcp:// without TLS, or one more entry in insecure-registries, send it through a security review rather than a quick edit on one node.