Post-deploy smoke test: byte-compare templated config files against what a running Docker container actually sees (single-file bind mount + atomic rewrite = stale inode bug)
Find a file
Lain Iwakura a658cf236b Guard host-side slurp with ansible_check_mode
A brand-new config path (e.g. a role restructuring where its file lives)
genuinely doesn't exist on disk under --check — the template task that
would create it doesn't actually write in dry-run. slurp failed hard on
that instead of skipping, caught deploying the nginx.conf directory-mount
migration to configure-hyperion-router.

Signed-off-by: Lain Iwakura <lain.iwakura@avallon.pw>
2026-08-08 18:29:30 +07:00
defaults Add docker-config-verify: post-deploy bind-mount staleness smoke test 2026-08-08 14:03:23 +07:00
meta Add docker-config-verify: post-deploy bind-mount staleness smoke test 2026-08-08 14:03:23 +07:00
tasks Guard host-side slurp with ansible_check_mode 2026-08-08 18:29:30 +07:00
README.md Add docker-config-verify: post-deploy bind-mount staleness smoke test 2026-08-08 14:03:23 +07:00

docker-config-verify

Post-deploy smoke test for any Docker service configured via ansible-templated files. Catches the specific bug class where a single-file bind mount (host/file:/container/file, as opposed to a directory bind) is pinned to a stale inode after ansible.builtin.template (or any other tempfile+rename atomic writer) replaces the host file — the container keeps serving old content forever, no matter how many times you reload, until it's recreated. nginx -t-style syntax checks don't catch this on their own: stale-but-valid content passes them silently. See nikki-memory tech_notes.md: "Docker — single-file bind mount + atomic rewrite = stale content".

Usage

Include it right after the tasks that template your config files and notify a reload/restart handler for them — it flushes those handlers first, so it's checking genuinely post-deploy state, not what was there before this run.

- name: Deploy nginx.conf
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: "{{ nginx_router_install_dir }}/nginx.conf"
  notify: restart nginx-router   # single-file mount -> needs restart, not reload

- name: Verify config reached the running container
  ansible.builtin.include_role:
    name: docker-config-verify
  vars:
    docker_config_verify_container: nginx-router
    docker_config_verify_files:
      - host: "{{ nginx_router_install_dir }}/nginx.conf"
        container: /etc/nginx/nginx.conf
    docker_config_verify_test_command: nginx -t

Fails the play with a clear message (naming the exact file and the fix) if the container's view of any listed file doesn't match the host's rendered copy. Files bind-mounted as a directory (e.g. conf.d/, letsencrypt/) don't need this — a directory bind mount does a fresh path lookup on every open(), so reload always sees current content there regardless of how the file underneath was written.

Variables

Variable Required Default Description
docker_config_verify_container yes "" container name/ID to check
docker_config_verify_files yes [] list of {host, container} path pairs to byte-compare
docker_config_verify_test_command no "" command run inside the container after the comparison passes (e.g. nginx -t); skipped if empty
docker_config_verify_fail_hint no "" extra line appended to the failure message

Why the file comparison runs before the test command

A stale single-file mount can still be syntactically valid — e.g. if the only change was a comment or a value tweak, nginx -t on the old content passes just fine, and you'd never know the change didn't apply. The byte comparison is the real signal; the test command is a secondary check for actual syntax errors in what you just deployed.