← Back to Wiki
Docker / Debugging

Config Changes That Silently Never Applied: Two Real Ways It Happens

You edit a config file. You reload the service. The command exits cleanly. Nothing about the running behaviour changes, and you go looking for a bug in your config. Here are two mechanisms that produce that exact experience, both hit in the same fleet, both leaving the previous configuration serving happily while every signal says you succeeded.

Share on X

One: a reload that talks to an API you turned off

Caddy's caddy reload does not signal the process. It makes an HTTP request to Caddy's admin API on localhost:2019. Perfectly reasonable, until the config contains:

{
	admin off
}

Now there is no admin API to talk to, and reload can never work on that host, no matter how correct your Caddyfile is:

sudo systemctl reload caddy
# dial tcp [::1]:2019: connect: connection refused

This one at least fails loudly and leaves the previous config serving, so you get an error and no outage. That is a much better failure mode than the silent variant below. The correct action is a restart, with validation first:

caddy validate --config /etc/caddy/Caddyfile
sudo systemctl restart caddy
BE WARNED: the silent version of this exists too. On a host where the admin API is enabled, caddy reload can log "config is unchanged" and skip applying your new block, while the file on disk is definitely different and caddy validate parses it fine. Same outcome, no error at all. If a reload does not take effect, stop retrying the reload and restart the process.

Two: a single-file bind mount and an inode-replacing write

This is the nastier one, because nothing fails anywhere.

Docker bind mounts are resolved at container start. Mount a directory and the container sees whatever is in it, always. Mount a single file and the container is bound to that file's inode, not its path.

volumes:
  - /opt/caddy/Caddyfile:/etc/caddy/Caddyfile   # single file: bound to the inode

Now consider how ordinary tools write files. sed -i does not edit in place despite the name. It writes a temporary file and renames it over the target, which produces a new inode. So do mv, most editors, and any "write new file, then replace" pattern.

Your host path now points at the new inode with your changes. The container is still holding the old one. Both files exist, both are on disk, and the container will read the stale one until it is recreated. No error, ever.

# prove it: the two should be identical, and are not
md5sum /opt/caddy/Caddyfile
docker exec caddy md5sum /etc/caddy/Caddyfile

The fix is to write through the existing inode rather than replacing it:

# truncate and rewrite in place. Same inode, container sees it
cat /tmp/Caddyfile.new > /opt/caddy/Caddyfile

# or with sed, keeping the original inode
sed 's/old/new/' /opt/caddy/Caddyfile | sponge /opt/caddy/Caddyfile
Better: bind-mount the directory, not the file. Single-file bind mounts are the root of this entire class of problem. Mount /opt/caddy at /etc/caddy and every write method works, because path resolution happens per-access rather than once at container start.

The habit that catches both

Never verify a config change by looking at the file you just edited. That file is the input. Ask the running process what it is using:

The general form is that a config file, a validated config file, and the config a process is actually running are three different things, and only the third one matters.

Two more places the same shape shows up

When this isn't your problem