Fix the Vaultwarden readiness probe

The first deploy failed its wait. The app was up -- Rocket had launched on port
80 and systemd reported the unit active -- but `podman healthcheck run` kept
returning non-zero. Podman's healthcheck plumbing is unreliable for
Quadlet-managed rootless containers: the check is registered against the *user*
systemd manager, and when that fails the check never runs, leaving `podman
healthcheck run` to report that the container has no healthcheck at all even
though the image declares one (upstream issues #26066, #25034, #22804).

Exec the image's own /healthcheck.sh instead: the identical probe (curl
http://localhost:80/alive) with no plumbing in between, and still nothing the
play has to invent. The cost is that the path is hardcoded, which the image's
Dockerfile pins, so a moved script fails as a plain 127 rather than hanging.

The wait is now a block with a rescue that prints the probe's own stdout/stderr,
the container's health status and the last 20 log lines. The bare "non-zero
return code" the plain task reported is exactly the message that hides the
reason.

The readme's verification commands and a new gotcha record why the probe is
exec'd, so nobody "fixes" it back to `podman healthcheck run`.
This commit is contained in:
tanshu committed 2026-10-16 16:15:38 +05:30
1 parent ecb8913153
commit 17c64cf2f8
2 files changed
+63 -12

No files matched your search

+49 -11
View File
@@ -85,17 +85,55 @@
# answering, and `Restart=always` means even a crash-looping container is
# reported active.
#
# `podman healthcheck run` executes the image's *own* HEALTHCHECK — which is
# /healthcheck.sh curling http://localhost:80/alive — so this depends on nothing
# the play has to know about: no curl, wget or python3 inside the image, and no
# hardcoded path that a later image tag could move.
- name: Wait for Vaultwarden to pass its own health check
ansible.builtin.command: "podman healthcheck run {{ container_name }}"
register: bitwarden_health
until: bitwarden_health.rc == 0
retries: 24
delay: 5
changed_when: false
# The probe is the image's own /healthcheck.sh — the script its HEALTHCHECK runs,
# curling http://localhost:80/alive — but it is exec'd directly rather than via
# `podman healthcheck run`. Podman's healthcheck plumbing is unreliable for
# Quadlet-managed rootless containers: the check is registered against the *user*
# systemd manager, and when that fails the check never runs, and `podman
# healthcheck run` reports that the container has no healthcheck at all even
# though the image declares one (upstream issues #26066, #25034, #22804). That is
# exactly what happened on the first deploy here: the app was serving, and the
# probe wrapper was the only thing failing.
#
# Exec'ing the script is the identical probe with no plumbing in between, and it
# still depends on nothing the play has to invent — no curl/wget/python3 of its
# own, just the script the image already ships. The cost is that the path is
# hardcoded; the image's Dockerfile pins it (`COPY docker/healthcheck.sh
# docker/start.sh /`), and if a future tag moves it the failure is a plain 127,
# whose stderr the rescue below prints.
- name: Wait for Vaultwarden to answer its own health check
block:
- name: Wait for Vaultwarden to answer its own health check
ansible.builtin.command: "podman exec {{ container_name }} /healthcheck.sh"
register: bitwarden_health
until: bitwarden_health.rc == 0
retries: 24
delay: 5
changed_when: false
rescue:
# A bare "non-zero return code" hides the one line that matters — curl's
# reason, or "no such file or directory" for a moved script — so pull the
# probe's own output and what the app is saying.
- name: Collect readiness diagnostics
ansible.builtin.command: "{{ item }}"
loop:
- "podman exec {{ container_name }} curl -sS http://localhost:80/alive"
- "podman inspect {{ container_name }} --format '{% raw %}{{ .State.Health.Status }}{% endraw %}'"
- "podman logs --tail 20 {{ container_name }}"
register: bitwarden_readiness_diag
changed_when: false
failed_when: false
- name: Show readiness diagnostics
ansible.builtin.debug:
msg:
- "probe stdout: {{ bitwarden_health.stdout | default('(none)') }}"
- "probe stderr: {{ bitwarden_health.stderr | default('(none)') }}"
- "{{ bitwarden_readiness_diag.results | map(attribute='stdout') | list }}"
- name: Fail
ansible.builtin.fail:
msg: "Vaultwarden is running but did not answer /alive — see the diagnostics above."
# The database is the whole vault, and it arrives over the network that
# roles/postgres attached Postgres to. Check the name the environment file