CoursesDocker in depthConfiguring the daemon

Configuring the daemon

daemon.json and engine defaults.

Intermediate12 min · lesson 6 of 30

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.

/etc/docker/daemon.json
{
"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.

terminal
$ sudo dockerd --validate --config-file /etc/docker/daemon.json
configuration 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.

terminal
$ docker run -d --name web nginx:1.27
c4e1b9f0a2d3f6a1b8c7d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0
$ 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.

terminal
$ docker network create net-a
7f3a9c2e1b8d4f60a5c7e9203d6b8f014e2a9c7d5b0f3a86c1d4e70b92a6f358
$ docker network create net-b
4c8e1a096f2b7d53b0a4c6e812f9d3a78e5c0b46a37f1d920d6b4e8c5f1a2c70
$ 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
Docker's default subnets can land on top of your own network
Left alone, the daemon leases new networks out of 172.17.0.0/16, 172.18.0.0/16, and the ranges immediately above them. If your office network, a VPN (virtual private network, the encrypted tunnel that puts your laptop on a company network from anywhere), or a cloud subnet already sits on one of those, containers start claiming addresses the host also needs to route to, and traffic to internal services quietly disappears. For the first hour it looks like a name-resolution problem, or a firewall rule someone forgot. It is an address clash. Point default-address-pools at a block you know is free (a slice of 10.x, say) and the whole class of bug goes away. Run ip route on the host before you pick, so you can see what is already spoken for.
What a restart does with your daemon.json
systemctl restart docker
the daemon re-reads /etc/docker/daemon.json
valid JSON
dockerd starts
new defaults apply to every new container; running ones survive with live-restore
typo or bad key
dockerd exits
the socket never opens, so docker commands fail with cannot connect
This is why you run dockerd --validate first and keep a known-good copy. If the daemon is down, journalctl -u docker prints the exact line it choked on.

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.

terminal
$ 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.

Quick check
01You set log-opts with max-size and max-file in daemon.json and restart the engine. A week later somebody starts a container and passes no --log-opt flags at all. What happens to that container's logs?
Incorrect — No. Setting it in daemon.json exists precisely so containers get rotation without carrying flags of their own.
Correct — daemon.json is engine-wide policy, so a container that passes no log flags inherits the defaults automatically.
Incorrect — No. Leaving the log flags off is normal, and the container falls back to the engine default.
Incorrect — No. Compose can override the default, but the daemon-wide setting is already doing its job.
02You have edited daemon.json and you are about to restart the daemon. What does running dockerd --validate --config-file /etc/docker/daemon.json actually buy you?
Incorrect — Validation applies nothing. The daemon only picks up changes when it restarts.
Incorrect — --validate neither restarts nor rolls back. It reads the file, nothing more.
Correct — It checks the file on its own, which is how you find the mistake before dockerd tries to load it.
Incorrect — It looks at the config file itself, not at running containers.
03Containers on a brand-new host cannot reach an internal service at 172.18.4.10. Everything else they talk to is fine, so it smells like DNS or a firewall rule. What is really going on, and what fixes it?
Incorrect — Log rotation has nothing to do with reaching a network address.
Correct — A clash between Docker's default pool and a real network swallows the traffic without printing a single error.
Incorrect — The storage driver decides how layers stack on disk, not where packets go.
Incorrect — live-restore governs whether containers survive a daemon bounce. It has nothing to say about subnet allocation.

Related