Stop the health wait from failing a healthy deploy, and clear a stale address

The first real deploy left the site on 502 and this is why. podman builds an OCI
image by default and drops the Docker HEALTHCHECK instruction, so the app
container has no State.Health at all; the role waited on that field, exhausted
its retries and failed the play, so the caddy role never ran and Caddy kept
proxying menu.hopsngrains.com to the old nginx container that is now gone.

The wait now asks the app itself: docker exec node -e fetch /healthz, which needs
no healthcheck and no DNS. And because the address may still be claimed by the
snippet the retired nginx-menu playbook wrote, the caddy role drops that marker
before writing its own - two blocks for one address make Caddy refuse the config,
and a refused reload quietly leaves the old proxy in place.

check-site also asked for the font the old design used, so it would have failed a
healthy site; it checks one we ship.
This commit is contained in:
tanshu committed 2026-09-22 23:44:54 +05:30
1 parent 25ef94d7af
commit 1923c80298
3 files changed
+32 -16

No files matched your search

+11
View File
@@ -11,6 +11,17 @@
Caddy container and its config, before this one.
when: not menu_caddyfile.stat.exists
# The Site's web address may still be claimed by an older snippet: the retired
# nginx-menu playbook wrote one per hostname. Two blocks for one address make
# Caddy refuse the whole config, and a refused reload leaves the old proxy in
# place, which is a 502 once the old container is gone.
- name: Drop any older snippet claiming this web address
blockinfile:
path: '{{ caddyfile_path }}'
marker: '# {mark} Ansible managed Caddy snippet for {{ host }}'
state: absent
notify: Reload Caddy
# blockinfile replaces whatever sits between its markers, so redeploying updates
# the snippet in place rather than appending a second one.
- name: Point the Site's web address at the app
+20 -15
View File
@@ -61,27 +61,32 @@
mode: '0600'
when: menu_site_env.stat.exists
- name: Wait for the app to report healthy
# The image's own HEALTHCHECK fetches /healthz inside the container. Polling
# that is deliberate: a container name resolves only on the Docker network, so
# the host cannot fetch http://{{ docker_container }}:3000/healthz.
docker_container_info:
name: '{{ docker_container }}'
# Deliberately not the image's HEALTHCHECK: podman builds an OCI image by
# default and drops the Docker HEALTHCHECK instruction, so State.Health is absent
# and a wait on it fails on a perfectly healthy container — which stops the play
# before Caddy is ever pointed at the app.
- name: Wait for the app to answer inside its own container
ansible.builtin.command:
argv:
- docker
- exec
- '{{ docker_container }}'
- node
- -e
- "fetch('http://127.0.0.1:3000/healthz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
register: menu_health
changed_when: false
retries: 20
delay: 5
until: >-
menu_health.container is defined and
menu_health.container.State.Health is defined and
menu_health.container.State.Health.Status == 'healthy'
delay: 3
until: menu_health.rc == 0
failed_when: false
- name: Fail when the app never becomes healthy
- name: Fail when the app never answers
fail:
msg: >-
{{ docker_container }} did not report healthy within 100s. Check
`docker logs {{ docker_container }}`.
when: menu_health.container.State.Health.Status | default('unknown') != 'healthy'
{{ docker_container }} did not answer /healthz within a minute. Its logs
say why: docker logs {{ docker_container }}
when: menu_health.rc != 0
- name: Check the data directory for integrity problems
# Reads the same directory the container writes, as the same user, so a
+1 -1
View File
@@ -55,7 +55,7 @@ await checkPage('/robots.txt', ['Disallow: /admin']);
await checkPage('/admin/login', ['type="password"']);
await checkPage('/assets/styles.css', ['--copper']);
await checkPage('/assets/app.js', ['data-zoom']);
await checkPage('/assets/fonts/archivo-latin.woff2', []);
await checkPage('/assets/fonts/playfair-display-latin.woff2', []);
const missingPage = await request('/definitely-not-here');
record('GET /definitely-not-here', missingPage.status === 404, `status ${missingPage.status}`);