Skip to content
Latchkey LogoLatchkey home

Unable to configure the Docker daemon with file daemon.json in CI

Unable to configure the Docker daemon with file daemon.json is dockerd refusing to start because it could not turn your configuration file into a usable configuration. The sentence is only a wrapper: what actually went wrong is in the clause that follows the file name, and it is either the file failing to parse or an option being set in two places at once.

Diagram of dockerd merging flags and daemon.json and the two ways that merge fails
Flags and the file are merged before anything starts. A parse failure and a duplicate directive both stop it there.

What this error means

Every Docker command in the job fails to connect, and the reason is not in the job log at all, because the daemon never came up to write one. On a runner you provision yourself, this typically follows a provisioning step that wrote /etc/docker/daemon.json for a registry mirror, a log driver or a storage option, and the workflow that used to work now fails at its first container. The daemon logs the real line to the system journal, so the failure is invisible until you go and look there.

The line docker/for-linux#327 reports from dockerd (quoted, not a recorded run)
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: insecure-registries: (from flag: [192.168.0.2:5000], from file: [192.168.0.2:5000])

One wrapper, two very different failures underneath

The opening sentence is written once, in moby daemon/config/config.go, where the daemon wraps whatever went wrong while reading the file and names the path it was reading. It says nothing about the cause on its own. The cause is in the wrapped error that follows the colon, and there are two families of it.

The first is a JSON parse failure, and it is reported by the Go JSON decoder rather than by Docker, so the wording is the decoder's: an invalid character, and what it was looking for when it found it. The second is a conflict check that Docker performs itself after parsing, written in the same file, which refuses to start when an option is set both on the dockerd command line and in the configuration. The example above is that second kind, and it shows the two literals joined into one sentence at run time, which is why the whole line appears in no source file even though both halves are real.

Failure modeThe clause dockerd adds after the file name
The file is not valid JSONinvalid character ... from the Go decoder, naming what it expected
An option is set as a flag and in the filethe following directives are specified both as a flag and in the configuration file: ...
A key is not one this daemon knowsA rejection naming the key, so the spelling and the version are both worth checking
The file is absentNothing. A missing daemon.json is normal and the daemon starts with defaults

Common causes

The file is not valid JSON

A trailing comma, an unquoted key, a smart quote pasted from a document, or a stray brace. The Go decoder reports the character and what it expected, which is enough to find the spot, and the daemon refuses to start rather than guessing at what you meant.

An option is set both as a flag and in the file

The daemon checks for this deliberately and aborts rather than picking a winner. The classic is a registry or host setting present in a systemd unit override and in daemon.json at the same time, usually because two different guides were followed months apart.

A key is misspelled or not supported by this version

Configuration keys change between Docker releases, and a key copied from a newer version of the documentation onto an older daemon is rejected at startup. In our experience this is the cause when the file is obviously valid JSON and the message still names the file rather than a parse position.

A provisioning step appended to the file with text tools

Shell redirection that adds a key to an existing JSON object produces a file that looks right and is not. This is the mechanism behind most of the parse failures above, and it is worth naming separately because the fix is to change how the file is written, not just to repair it once.

How to fix it

Validate the file in the step that writes it

  1. Parse the file with a real JSON parser immediately after writing it.
  2. Fail the provisioning step on a parse error, before anything restarts the daemon.
  3. Only then restart Docker, and check the unit actually came back up.
Terminal
sudo tee /etc/docker/daemon.json >/dev/null <<'JSON'
{ "registry-mirrors": ["https://mirror.example.internal"] }
JSON
python3 -c "import json; json.load(open('/etc/docker/daemon.json'))"
sudo systemctl restart docker && sudo systemctl is-active docker

Keep every option in one place

Pick either the systemd unit flags or daemon.json for each setting and remove it from the other. The daemon refuses to start when the same directive appears twice, so a duplicate is not a style problem that can wait; it is the whole failure.

Terminal
systemctl cat docker | grep -n ExecStart
sudo cat /etc/docker/daemon.json

Edit JSON with a JSON tool

Use a parser to add or change keys, so the file is re-emitted as valid JSON every time instead of being patched as text. This removes the trailing comma class of failure permanently rather than fixing one instance of it.

Terminal
tmp=$(mktemp)
jq '."log-driver" = "json-file"' /etc/docker/daemon.json > "$tmp" \
  && sudo mv "$tmp" /etc/docker/daemon.json

Surface the daemon journal in the failing job

Add a failure-only step that prints whether the unit is active and the tail of its journal. A workflow that cannot reach the daemon then says why in its own log, which is the difference between a five minute fix and an afternoon.

Terminal
systemctl is-active docker || journalctl -u docker --no-pager -n 40

The trailing comma is the one that catches everyone

JSON does not allow a comma after the last entry in an object, and almost every other configuration format in a pipeline does. A provisioning script that appends a key to daemon.json with a shell fragment, rather than by parsing and re-emitting the file, is the usual way one arrives. The daemon then fails to start on the next restart, which may be days later, so the change and the breakage do not look related.

Validate the file as JSON before restarting anything, in the same step that wrote it. This is a one line check with any JSON parser you already have on the machine, and it turns a daemon that will not start into a provisioning step that failed loudly at the point of the mistake.

Terminal
# validate before you restart, every time
python3 -c "import json,sys; json.load(open('/etc/docker/daemon.json')); print('daemon.json parses')"
sudo systemctl restart docker
sudo systemctl is-active docker

Read the journal, because the job log will not have it

The single most useful habit with this failure is knowing where the message lives. dockerd writes it to its own standard error, which systemd collects, so it is in the unit journal and nowhere near the workflow that failed. A job that only reports that it cannot connect to the daemon is telling you the truth and is several layers away from the cause.

On a self-hosted runner, wire that into the pipeline: if the daemon is not active, print the last lines of its journal in the job. The failing workflow then carries its own explanation instead of requiring somebody to log into the machine.

.github/workflows/ci.yml
- name: Explain a dead daemon in the job that hit it
  if: failure()
  run: |
    systemctl is-active docker || sudo journalctl -u docker --no-pager -n 40
    sudo cat /etc/docker/daemon.json || echo "no daemon.json on this host"

What the runner does about it

No repair, and no recorded run, and the reason is structural rather than a choice. To record this we would need a job log showing it, and there cannot be one: the failure happens while the daemon is starting, which is before any job is dispatched to that machine. A runner whose daemon did not come up does not run your workflow and then print this, it simply never becomes available. There is no run to capture because the capture would have to happen on a machine that never ran anything.

That is also why the fixes here are all provisioning fixes. This failure belongs to whoever writes the host configuration, and on a managed runner that is not you. If you are hitting it, you are on a runner you provision, and the validation step above belongs in the provisioning rather than in the workflow.

How to prevent it

  • Validate daemon.json as JSON in provisioning, before any restart.
  • Keep each daemon option in exactly one of the unit file and daemon.json.
  • Edit the file with a JSON tool rather than with shell redirection.
  • Check the unit is active after restarting, not just that the restart command returned.

Frequently asked questions

Where do I find the real error when the daemon will not start?
In the daemon unit journal, not in the workflow log. dockerd writes the message to its standard error while starting, so systemd captures it and the job only ever sees that it cannot connect. journalctl -u docker with a small line count is where to look first.
What does "specified both as a flag and in the configuration file" mean?
That the same option appears on the dockerd command line, usually through a systemd unit override, and in daemon.json. The daemon refuses to choose between them and aborts instead. Remove the directive from one of the two places and it will start.
Why does the full error message not appear anywhere in the Docker source?
Because it is two literals joined at run time. The opening sentence about configuring the daemon with a file and the clause about duplicated directives are separate format strings in moby daemon/config/config.go, and the error wrapper glues them with a colon when both happen. Searching either half finds the code; searching the whole line does not.
Is a missing daemon.json a problem?
No. The file is optional, and a daemon with no configuration file starts on its defaults. That is why the error always names a path: it is reporting a file it found and could not use, never a file it wanted and did not find.

Related guides

References

Nobody configures a daemon on a runner they do not maintain. Latchkey runners are $0.0025/min at 2 vCPU. Start free → 30-day trial · No credit card