From 5143840d90669f1fbe5c0d125e188f40e48d474a Mon Sep 17 00:00:00 2001 From: jhodgkin Date: Sun, 12 Jul 2026 22:06:52 -0600 Subject: [PATCH] fix: bind-mount config/ as a directory, not a single file Single-file bind mounts pin the container to that file's inode at mount time. sed -i and most editors write-then-rename (atomic write), which swaps in a new inode at the same path -- the container kept reading the orphaned original and never saw edits, silently breaking the hot-reload from the previous commit. Caught by actually testing the reload live instead of trusting the code. Directory mounts resolve paths dynamically and don't have this problem. Co-Authored-By: Claude Sonnet 5 --- docker-compose.yml | 8 +++++++- docs/hot-reload.md | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 9f3b1a2..1b5037b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,13 @@ services: - SSH_PRIVATE_KEY_PATH=/app/ssh/monitor_ed25519 - DISCOVERY_FILE_PATH=/app/data/devices-raw.json volumes: - - ./config/hosts.yaml:/app/config/hosts.yaml:ro + # Directory mount, not a single-file mount: bind-mounting one file pins + # the container to that file's inode at mount time. Editors/`sed -i` + # that write-then-rename (the common atomic-write pattern) swap in a new + # inode at the same path, which the container would never see again + # without a restart — silently breaking the hot-reload in issue #14. + # Directory mounts resolve paths dynamically and don't have this problem. + - ./config:/app/config:ro - ./ssh/monitor_ed25519:/app/ssh/monitor_ed25519:ro # Bind mount (not a named volume) so scripts/discover-devices.sh, which # runs on the host via systemd (see deploy/systemd/), can write diff --git a/docs/hot-reload.md b/docs/hot-reload.md index 96b3cec..8556a2c 100644 --- a/docs/hot-reload.md +++ b/docs/hot-reload.md @@ -16,3 +16,16 @@ cycle (checks the file's mtime, reloads if changed) — no container restart nee If the file fails to parse (bad YAML), the error is logged and the **previous** in-memory config keeps running rather than crashing the poller. + +## Gotcha that broke this on first deploy + +`docker-compose.yml` originally bind-mounted the single file +(`./config/hosts.yaml:/app/config/hosts.yaml:ro`). Single-file bind mounts pin the +container to that file's **inode** at mount time. `sed -i`, most text editors, and any +tool that does an atomic write (write to a temp file, then `rename()` over the +original — the common safe-write pattern) swap in a *new* inode at the same path. The +container kept reading the original (now-orphaned) inode and never saw edits made this +way, no matter how long you waited for the next poll cycle. Fixed by mounting the +**directory** instead (`./config:/app/config:ro`), which resolves paths dynamically +rather than pinning to a specific inode. Confirmed via `docker exec ... stat` showing a +stale mtime that didn't match the host file before this fix.