The docker CLI is what most people interact with daily, but almost everything that actually matters — where images and containers are stored, how logging works, which network ranges are used, resource limits, security defaults — is controlled by the daemon, dockerd, and its configuration file. This guide walks through the daemon’s configuration surface end to end: where it lives, how to change it safely, and the options you’ll actually use in production.
Where Daemon Configuration Lives
On Linux, the daemon reads its configuration from /etc/docker/daemon.json, a plain JSON file. If it doesn’t exist yet:
sudo mkdir -p /etc/docker
sudo touch /etc/docker/daemon.json
Check what’s currently configured:
cat /etc/docker/daemon.json
If empty or missing, Docker runs with sane built-in defaults — you only need this file to override them.
You can also inspect the daemon’s live, effective configuration (merged defaults + file + any command-line flags):
docker info
Editing and Reloading Configuration
After editing daemon.json, validate the JSON syntax before restarting (a syntax error will prevent the daemon from starting at all):
python3 -m json.tool /etc/docker/daemon.json
Reload the daemon:
sudo systemctl reload docker
Some settings support a live reload via SIGHUP without a full restart (e.g., log level, registry mirrors, insecure registries, max concurrent downloads); others (like data-root or the storage driver) require a full restart:
sudo systemctl restart docker
Check the daemon actually came back up cleanly:
sudo systemctl status docker
journalctl -u docker --since "5 minutes ago"
Core Configuration Options
Changing the Data Root
By default, Docker stores images, containers, and volumes under /var/lib/docker. On many servers this fills the root partition — moving it to a larger disk is one of the most common daemon config changes:
{
"data-root": "/mnt/docker-data"
}
Stop Docker, move the existing data, then restart:
sudo systemctl stop docker
sudo rsync -aP /var/lib/docker/ /mnt/docker-data/
sudo mv /var/lib/docker /var/lib/docker.old
sudo systemctl start docker
Verify:
docker info --format '{{.DockerRootDir}}'
/mnt/docker-data
Once confirmed working, remove the old directory to reclaim space.
Configuring Logging Defaults
By default, Docker uses the json-file log driver with no size limit — a chatty container can fill your disk over time. Set sane defaults globally:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
This caps each container’s logs at 3 rotated files of 10MB each. Confirm per-container after restart:
docker inspect --format '{{.HostConfig.LogConfig}}' <container_id>
{json-file map[max-file:3 max-size:10m]}
For centralized logging, switch drivers entirely, e.g. to send logs to a syslog server:
{
"log-driver": "syslog",
"log-opts": {
"syslog-address": "udp://logs.internal:514"
}
}
Setting the Storage Driver
Modern Docker installs default to overlay2 on most Linux distributions, which is almost always the right choice. Confirm what’s active:
docker info --format '{{.Driver}}'
overlay2
Override only if you have a specific reason (e.g., certain older filesystems):
{
"storage-driver": "overlay2"
}
Registry Mirrors and Insecure Registries
If you run a local pull-through cache or a private registry without valid TLS certs (lab environments only):
{
"registry-mirrors": ["https://mirror.internal.example.com"],
"insecure-registries": ["registry.internal:5000"]
}
Default Address Pools (Avoiding Network Overlaps)
If you run many Compose projects or Swarm networks, Docker’s default bridge subnet ranges can collide with your existing infrastructure. Define custom pools:
{
"default-address-pools": [
{"base": "172.30.0.0/16", "size": 24},
{"base": "172.31.0.0/16", "size": 24}
]
}
Limiting Concurrent Downloads/Uploads
On constrained networks, tune registry throughput:
{
"max-concurrent-downloads": 3,
"max-concurrent-uploads": 3
}
Enabling Experimental Features
Some features (like certain BuildKit capabilities or checkpoint/restore) ship behind an experimental flag:
{
"experimental": true
}
Setting Default Runtime and Adding Custom Runtimes
If you’re using gVisor, Kata Containers, or NVIDIA’s container runtime for GPU workloads:
{
"default-runtime": "runc",
"runtimes": {
"nvidia": {
"path": "/usr/bin/nvidia-container-runtime"
}
}
}
Use a non-default runtime per container:
docker run --runtime=nvidia --rm nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi
A Complete Production-Style daemon.json
Putting several of the above together into a realistic configuration:
{
"data-root": "/mnt/docker-data",
"storage-driver": "overlay2",
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-address-pools": [
{"base": "172.30.0.0/16", "size": 24}
],
"max-concurrent-downloads": 5,
"live-restore": true,
"userland-proxy": false,
"metrics-addr": "127.0.0.1:9323",
"experimental": false
}
A few notable entries worth calling out:
live-restore: true— keeps containers running even if the daemon itself crashes or is upgraded, by relying oncontainerd‘s independent process supervision.userland-proxy: false— disables Docker’s userspace TCP/UDP proxy for published ports, relying purely oniptablesNAT rules instead, which is faster and reduces one moving part.metrics-addr— exposes Prometheus-compatible daemon metrics on a local-only address.
Command-Line Flags vs daemon.json
Everything in daemon.json has an equivalent dockerd command-line flag, but mixing both is a common source of the “why won’t my config apply” problem — if systemd starts dockerd with explicit -H or other flags, they can conflict with or override daemon.json. Check what your systemd unit actually runs:
systemctl cat docker.service
If you see ExecStart=/usr/bin/dockerd -H fd:// with additional flags, and you want daemon.json to be the single source of truth, override it:
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/override.conf <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd
EOF
sudo systemctl daemon-reexec
sudo systemctl restart docker
Monitoring Daemon Health
Check resource usage and general daemon state at a glance:
docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 12 8 4.2GB 1.8GB (42%)
Containers 8 5 120MB 45MB (37%)
Local Volumes 6 4 2.1GB 0.5GB (23%)
Prune unused resources periodically (schedule via cron for hosts that accumulate cruft):
docker system prune -af --volumes
Stream daemon-level events for observability tooling:
docker events --filter type=container
Internal Working: How the Daemon Applies Configuration
At startup, dockerd merges configuration in this order of precedence: built-in defaults, then daemon.json, then command-line flags (flags win on conflict — which is exactly why leftover systemd flags silently override your JSON file). Once merged, the daemon initializes each subsystem — the storage driver, the network controller (libnetwork), the logging subsystem, and the runtime shim manager — using that final configuration. Some subsystems (logging level, registry config) support runtime reconfiguration via SIGHUP; others are wired in at process start and require a full restart to change, which is why data-root and storage-driver changes need systemctl restart rather than reload.
Best Practices
- Keep
daemon.jsonas the single source of truth; avoid duplicating settings via systemd override flags. - Always cap log file sizes (
max-size/max-file) — an unbounded log driver is one of the most common causes of Docker hosts unexpectedly running out of disk. - Set
live-restore: truein production so daemon upgrades or crashes don’t take down running workloads. - Version-control your
daemon.json(e.g., via Ansible/Terraform/config management) rather than hand-editing production hosts. - Validate JSON syntax before every restart — a broken
daemon.jsonwill prevent Docker from starting, potentially taking down every container on the host if it also restarts.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Docker won’t start after config change | Invalid JSON syntax | Validate with python3 -m json.tool daemon.json before restarting |
| Config changes seem to have no effect | Systemd unit overriding with its own flags | Clear ExecStart via systemd override as shown above |
Disk fills up despite max-size set | Setting only applied going forward, not retroactively | New containers respect it; recreate long-running containers to pick up new log limits |
data-root change loses existing images | Data not migrated before restart | Always rsync existing data to the new path before restarting with the new data-root |
Summary
The Docker daemon’s behavior — where it stores data, how it logs, which networks it hands out, which runtimes it supports — is fully controllable through /etc/docker/daemon.json. Understanding the precedence between defaults, config file, and command-line flags (and clearing any systemd-injected flags that conflict with your file) is the key to configuration changes actually taking effect reliably.
References
- Docker daemon configuration reference: https://docs.docker.com/engine/reference/commandline/dockerd/
- dockerd JSON configuration file reference: https://docs.docker.com/engine/daemon/
- Docker logging drivers: https://docs.docker.com/engine/logging/configure/
- Docker storage drivers: https://docs.docker.com/engine/storage/drivers/