| |

Docker 49 🐳 Docker Daemon Configuration: Customizing /etc/docker/daemon.json

The Docker daemon is the background process that builds, runs, and manages every container on the host. It listens on the socket, pulls the images, starts the containers, and writes the logs. By default, it works well enough for the basic use, but the production environment demands the control over the logging, the storage, the networking, and the security. The primary way to configure the daemon is the single JSON file, the /etc/docker/daemon.json, which is the cleaner and the easier to version-control than the long list of the startup flags. The file is read when the daemon starts, and the options apply to the containers created after the restart.

The configuration file is not the only way to configure the daemon, but it is the recommended. The command-line flags are the alternative, and the two can be combined. The catch is that the same option must not appear in both the file and the flags, because the daemon detects the conflict and refuses to start. The error message names the conflicting option, and the fix is to remove one of the two. The JSON file is the declarative, and the flags are the imperative, and the modern Docker prefers the declarative.

The options in the file cover the wide range. The data-root moves the entire Docker data directory to a different disk. The log-driver and the log-opts control the logging and the rotation. The registry-mirrors and the insecure-registries control the image pulls. The default-ulimits and the host-gateway-ip control the resource limits and the host networking. Each option is the key in the JSON object, and the values are the strings, the numbers, or the nested objects.

This chapter covers three areas. First, why the daemon configuration exists — the default-versus-production gap and the declarative approach. Second, the file location and the key options — the daemon.json path, the storage, the logging, the registry, and the security. Third, the application and the verification — the systemctl reload, the docker info, the docker inspect, and the the common pitfalls. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the configuration flow.

Key point: The /etc/docker/daemon.json file is the central configuration for the Docker daemon. The data-root moves the data directory, the log-driver and the log-opts control the logging, the registry-mirrors and the insecure-registries control the pulls, and the default-ulimits controls the resource limits. The same option must not be set in both the file and the command-line flags. The changes apply only after the daemon restart, and only to the containers created after the restart.


Why the daemon configuration exists

The default-versus-production gap. The Docker defaults are tuned for the convenience, not the production. The json-file logging driver has no rotation, and a busy host fills the disk within days. The default data root is the /var/lib/docker on the root partition, which may be the small SSD. The default registry is the Docker Hub, which may be the slow or the blocked. The production requires the different choices, and the daemon.json is where they are made.

The declarative approach. The daemon can be configured with the command-line flags, but the flags are the long, the error-prone, and the hard to version-control. The daemon.json is the single file, and it can be committed to the configuration management, the Ansible, the Puppet, or the Git. The declarative file is the auditable, and the flags are the ephemeral.

The apply-on-restart problem. The daemon reads the daemon.json at the startup. The change to the file does not affect the running daemon until the restart, and the restart does not affect the existing containers. The existing containers keep the driver, the options, and the data root they were created with. The container must be recreated to pick up the new daemon defaults. The distinction is the source of the “why did my change not apply” confusion.

The conflict problem. The daemon merges the daemon.json and the command-line flags. If the same option is set in both, the daemon cannot decide which one wins, and it refuses to start. The error message names the option, and the fix is to remove one. The conflict is the common mistake, and the journalctl -u docker shows the error.

The live-reload problem. A subset of the options can be reloaded without the restart, and the SIGHUP triggers the reload. The live-restore, the debug, the labels, the max-concurrent-downloads, the max-concurrent-uploads, the default-runtime, the runtimes, the authorization-plugins, the insecure-registries, the registry-mirrors, the shutdown-timeout, and the features are the reloadable. The other options require the full restart.

The trade-off. The daemon.json is the single file, and the single file means the single point of the failure. A malformed JSON breaks the daemon start. The typo in the key is the silent ignore, and the option is not applied. The validation is the manual, and the docker info is the verification. The trade-off is between the simplicity of the single file and the need for the careful editing.


a. The file location and the structure

The file location depends on the operating system and the way the Docker runs.

The environmentThe file location
Linux (the regular)/etc/docker/daemon.json
Linux (the rootless)~/.config/docker/daemon.json
Linux (the snap)/var/snap/docker/current/config/daemon.json
WindowsC:\ProgramData\docker\config\daemon.json

The file is the single JSON object, and the keys are the options.

{
  "data-root": "/mnt/docker-data",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "compress": "true"
  },
  "registry-mirrors": ["https://mirror.example.com"],
  "insecure-registries": ["myregistry.local:5000"],
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65536,
      "Soft": 65536
    }
  }
}

The file does not exist by default on the fresh installation, and the daemon uses the built-in defaults. The creation of the file with the {} is the starting point, and the options are the keys.

The merge with the flags. The daemon reads the daemon.json and the dockerd flags. The flags are the --log-driver, the --data-root, and the others. The two are merged, and the conflict is the error.

The JSON validation. The file must be the valid JSON. The trailing comma, the unquoted key, and the wrong bracket are the errors. The python -m json.tool /etc/docker/daemon.json validates the file, and the jq is the alternative.


b. The key options

The storage. The data-root moves the entire Docker data directory:

{
  "data-root": "/mnt/docker-data"
}

The move requires the stop of the daemon, the copy of the data, the update of the file, and the start of the daemon. The containerd image store keeps the image data in the /var/lib/containerd, and the data-root does not move it.

The logging. The log-driver and the log-opts control the logging:

{
  "log-driver": "local",
  "log-opts": {
    "max-size": "10m"
  }
}

The local is the recommended production default because it rotates by default. The json-file requires the explicit max-size and the max-file. The values inside the log-opts must be the strings.

The registry. The registry-mirrors and the insecure-registries control the pulls:

{
  "registry-mirrors": ["https://mirror.example.com"],
  "insecure-registries": ["myregistry.local:5000"]
}

The mirror is the cache for the Docker Hub, and the insecure registry is the private registry without the TLS.

The networking. The host-gateway-ip sets the IP that the host-gateway resolves to:

{
  "host-gateway-ip": "192.168.1.1"
}

The --add-host=host.docker.internal:host-gateway uses the value, and the container reaches the host.

The resource limits. The default-ulimits sets the default limits:

{
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65536,
      "Soft": 65536
    }
  }
}

The nofile is the open file descriptor limit, and the nproc is the process limit.

The live-restore. The live-restore keeps the containers running when the daemon restarts:

{
  "live-restore": true
}

The option allows the daemon upgrade without the container downtime, and it is the important for the production.

The debug. The debug enables the debug output:

{
  "debug": true
}

The debug is the verbose, and it is the temporary for the troubleshooting.


c. The application and the verification

The reload. The systemctl reload docker sends the SIGHUP and reloads the reloadable options. The systemctl restart docker restarts the daemon and applies all the options.

sudo systemctl daemon-reload
sudo systemctl restart docker

The daemon-reload reloads the systemd units, and the restart docker restarts the daemon. The two are the sequence after the daemon.json edit.

The verification. The docker info shows the applied options:

docker info --format '{{.LoggingDriver}}'
# local

docker info --format '{{.DockerRootDir}}'
# /mnt/docker-data

docker info --format '{{json .RegistryConfig.Mirrors}}'
# ["https://mirror.example.com"]

The docker info is the primary verification, and the docker inspect shows the per-container configuration.

The conflict detection. The journalctl -u docker shows the startup errors:

sudo journalctl -u docker -n 50

The conflict error names the option, and the fix is to remove one of the two.

The live-reload test. The kill -SIGHUP $(pidof dockerd) sends the signal, and the journalctl shows the reload messages.


Complete Example Session

// ============================================
// PART 1: THE BASIC FILE
// ============================================

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}


// ============================================
// PART 2: THE DATA ROOT
// ============================================

{
  "data-root": "/mnt/docker-data"
}

// The move requires the stop, the copy, and the start.


// ============================================
// PART 3: THE LOGGING
// ============================================

{
  "log-driver": "local",
  "log-opts": {
    "max-size": "10m",
    "max-file": "5"
  }
}

// The local driver rotates by default.


// ============================================
// PART 4: THE REGISTRY
// ============================================

{
  "registry-mirrors": ["https://mirror.example.com"],
  "insecure-registries": ["myregistry.local:5000"]
}


// ============================================
// PART 5: THE RESOURCE LIMITS
// ============================================

{
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65536,
      "Soft": 65536
    },
    "nproc": {
      "Name": "nproc",
      "Hard": 4096,
      "Soft": 4096
    }
  }
}


// ============================================
// PART 6: THE LIVE RESTORE
// ============================================

{
  "live-restore": true
}

// The containers keep running when the daemon restarts.


// ============================================
// PART 7: THE HOST GATEWAY
// ============================================

{
  "host-gateway-ip": "192.168.1.1"
}

// The --add-host=host.docker.internal:host-gateway uses it.


// ============================================
// PART 8: THE COMPLETE FILE
// ============================================

{
  "data-root": "/mnt/docker-data",
  "log-driver": "local",
  "log-opts": {
    "max-size": "10m",
    "max-file": "5",
    "compress": "true"
  },
  "registry-mirrors": ["https://mirror.example.com"],
  "insecure-registries": ["myregistry.local:5000"],
  "live-restore": true,
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65536,
      "Soft": 65536
    }
  }
}


// ============================================
// PART 9: THE APPLICATION
// ============================================

# The validate
python -m json.tool /etc/docker/daemon.json

# The reload
sudo systemctl daemon-reload
sudo systemctl restart docker

# The verify
docker info --format '{{.LoggingDriver}}'
docker info --format '{{.DockerRootDir}}'


// ============================================
// PART 10: THE LFCS PIPELINE
// ============================================

# The edit
sudo vi /etc/docker/daemon.json

# The validate
sudo python -m json.tool /etc/docker/daemon.json

# The apply
sudo systemctl daemon-reload
sudo systemctl restart docker

# The verify
docker info --format '{{json .}}' | jq '.LoggingDriver, .DockerRootDir'

# The conflict check
sudo journalctl -u docker -n 20 | grep -i conflict

# The live reload
sudo systemctl reload docker

The ten parts show the basic file, the data root, the logging, the registry, the resource limits, the live restore, the host gateway, the complete file, the application, and the LFCS pipeline.


Quick Reference

The File Locations

The environmentThe path
Linux (the regular)/etc/docker/daemon.json
Linux (the rootless)~/.config/docker/daemon.json
Linux (the snap)/var/snap/docker/current/config/daemon.json
WindowsC:\ProgramData\docker\config\daemon.json

The Common Options

The optionThe purpose
data-rootThe data directory
log-driverThe logging driver
log-optsThe driver options
registry-mirrorsThe pull mirrors
insecure-registriesThe non-TLS registries
default-ulimitsThe resource limits
live-restoreThe daemon restart
host-gateway-ipThe host gateway
debugThe debug output
default-runtimeThe runtime

The Reloadable Options

The optionThe reload
debugYes
labelsYes
live-restoreYes
max-concurrent-downloadsYes
max-concurrent-uploadsYes
registry-mirrorsYes
insecure-registriesYes
shutdown-timeoutYes
data-rootNo
log-driverNo

The Commands

The commandThe purpose
python -m json.toolThe validate
systemctl daemon-reloadThe systemd reload
systemctl restart dockerThe full restart
systemctl reload dockerThe SIGHUP reload
docker infoThe verification
journalctl -u dockerThe logs

Best Practices

✅ Do This:

// Use the local driver for the production
{
  "log-driver": "local",
  "log-opts": { "max-size": "10m" }
}                                                                          // ✅
// Use the live-restore for the production
{
  "live-restore": true
}                                                                          // ✅
// Use the mirror for the speed
{
  "registry-mirrors": ["https://mirror.example.com"]
}                                                                          // ✅
// Validate the JSON before the restart
// python -m json.tool /etc/docker/daemon.json                           // ✅
// Use the strings in the log-opts
"max-file": "3"                                                           // ✅

❌ Don’t Do This:

// Don't set the option in both the file and the flags
{"log-driver": "json-file"}  // and the --log-driver flag                // ❌
// Don't use the numeric value in the log-opts
{"max-file": 3}  // the string is required                               // ❌
// Don't forget the restart
// The change is not applied without the restart                         // ❌
// Don't expect the existing containers to change
// The recreate is required                                              // ❌
// Don't use the trailing comma
{"log-driver": "json-file",}  // the invalid JSON                        // ❌
// Don't move the data root with the running daemon
// The stop and the copy are required                                    // ❌

Common Pitfalls

The pitfallThe whyThe fix
The daemon does not startThe invalid JSONValidate with the python -m json.tool
The option is ignoredThe typo in the keyCheck the docker info
The conflict errorThe file and the flagsRemove one of the two
The change is not appliedThe no restartRestart the daemon
The existing containers unchangedThe container-scopedRecreate the container
The log rotation not workingThe json-file without the optsUse the local or the max-size
The data root not movedThe containerd storeCheck the /var/lib/containerd

Real-World Examples

1. The Log Rotation

{"log-driver": "json-file", "log-opts": {"max-size": "10m", "max-file": "3"}}

2. The Local Driver

{"log-driver": "local", "log-opts": {"max-size": "10m"}}

3. The Data Root

{"data-root": "/mnt/docker-data"}

4. The Registry Mirror

{"registry-mirrors": ["https://mirror.example.com"]}

5. The Insecure Registry

{"insecure-registries": ["myregistry.local:5000"]}

6. The Ulimits

{"default-ulimits": {"nofile": {"Name": "nofile", "Hard": 65536, "Soft": 65536}}}

7. The Live Restore

{"live-restore": true}

8. The Host Gateway

{"host-gateway-ip": "192.168.1.1"}

9. The Validate

python -m json.tool /etc/docker/daemon.json

10. The Apply

sudo systemctl daemon-reload && sudo systemctl restart docker

Visual

The Configuration Flow

┌──────────────────────────────────────────────────────────────┐
│  THE CONFIGURATION FLOW                                      │
│                                                              │
│  /etc/docker/daemon.json                                     │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  {                                                   │    │
│  │    "log-driver": "local",                            │    │
│  │    "data-root": "/mnt/docker-data"                   │    │
│  │  }                                                   │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  sudo systemctl daemon-reload                           │
│    │  sudo systemctl restart docker                          │
│    ▼                                                         │
│  The Docker daemon:                                          │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  Reads the file at the startup.                      │    │
│  │  Merges with the flags.                              │    │
│  │  Applies the options.                                │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    ▼                                                         │
│  The new containers:                                         │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The local driver, the new data root.                │    │
│  │                                                       │    │
│  │  The existing containers keep the old settings.      │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Default vs the Production

┌──────────────────────────────────────────────────────────────┐
│  THE DEFAULT vs THE PRODUCTION                               │
│                                                              │
│  THE DEFAULT:                                                │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The log-driver: the json-file (no rotation)         │    │
│  │  The data-root: the /var/lib/docker                  │    │
│  │  The registry: the Docker Hub                        │    │
│  │  The live-restore: the false                         │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  THE PRODUCTION:                                             │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The log-driver: the local (the rotation)            │    │
│  │  The data-root: the /mnt/docker-data                 │    │
│  │  The registry: the mirror                            │    │
│  │  The live-restore: the true                          │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The daemon.json is where the defaults become the            │
│  production choices.                                         │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Live Reload vs the Restart

┌──────────────────────────────────────────────────────────────┐
│  THE LIVE RELOAD vs THE RESTART                              │
│                                                              │
│  THE LIVE RELOAD (the SIGHUP):                               │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  systemctl reload docker                             │    │
│  │                                                       │    │
│  │  The reloadable options:                             │    │
│  │    the debug, the labels, the live-restore,          │    │
│  │    the registry-mirrors, the insecure-registries     │    │
│  │                                                       │    │
│  │  The containers keep running.                        │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  THE RESTART:                                                │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  systemctl restart docker                            │    │
│  │                                                       │    │
│  │  All the options are applied.                        │    │
│  │                                                       │    │
│  │  The containers stop and start (unless the           │    │
│  │  live-restore is enabled).                           │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Conflict Detection

┌──────────────────────────────────────────────────────────────┐
│  THE CONFLICT DETECTION                                      │
│                                                              │
│  The daemon.json:                                            │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  { "log-driver": "json-file" }                       │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The command line:                                           │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  dockerd --log-driver=local                          │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    ▼                                                         │
│  The error:                                                  │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  unable to configure the Docker daemon with file     │    │
│  │  /etc/docker/daemon.json: the following directives   │    │
│  │  are both in the file and on the command line:       │    │
│  │  log-driver                                           │    │
│  │                                                       │    │
│  │  The fix: remove one of the two.                     │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
/etc/docker/daemon.jsonThe daemon configuration
data-rootThe data directory
log-driverThe logging driver
log-optsThe driver options
registry-mirrorsThe pull mirrors
insecure-registriesThe non-TLS registries
default-ulimitsThe resource limits
live-restoreThe daemon restart
host-gateway-ipThe host gateway
The reloadsystemctl reload docker
The restartsystemctl restart docker

Key takeaways:

  • The /etc/docker/daemon.json file is the central configuration for the Docker daemon. The file is the declarative, and the command-line flags are the imperative. The two can be combined, but the same option must not appear in both .
  • The data-root moves the entire Docker data directory. The move requires the stop, the copy, and the restart. The containerd image store keeps the image data separately in the /var/lib/containerd .
  • The log-driver and the log-opts control the logging and the rotation. The local driver rotates by default, and the json-file requires the max-size and the max-file. The values inside the log-opts must be the strings .
  • The registry-mirrors and the insecure-registries control the pulls. The mirror speeds up the Docker Hub, and the insecure registry allows the private registries without the TLS .
  • The default-ulimits sets the default resource limits. The nofile is the open file descriptor limit, and the nproc is the process limit .
  • The live-restore keeps the containers running when the daemon restarts. The option allows the daemon upgrade without the container downtime, and it is the important for the production .
  • The changes apply only after the daemon restart. The systemctl restart docker applies all the options, and the systemctl reload docker applies only the reloadable ones. The existing containers keep the settings they were created with .
  • The conflict between the file and the flags prevents the daemon start. The journalctl -u docker shows the conflict error, and the fix is to remove one of the two .

Remember: The /etc/docker/daemon.json file is the central control panel for the Docker host. The data-root moves the data, the log-driver and the log-opts control the logging, the registry-mirrors and the insecure-registries control the pulls, and the default-ulimits controls the resource limits. The changes apply only after the restart, and the existing containers must be recreated. The result is the controlled, the observable, and the production-ready Docker host.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!