From 604a2458e9ce4c77a0af47bed86655ff19599130 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 23 May 2026 23:48:06 -0400 Subject: [PATCH] fix(makefile): add non-systemd podman healthcheck workaround Podman relies on systemd timers to schedule automatic healthchecks. On non-systemd systems (e.g., Void Linux with runit), healthchecks never fire, which causes podman-compose to hang forever waiting for service_healthy conditions that never resolve. Add two Make macros to handle this transparently: - compose_up: runs podman compose up -d normally on systemd, but with a 15-second timeout on non-systemd to create containers without hanging. Supports passing compose flags via $(call compose_up,args). - ensure_healthy: on non-systemd systems, waits for the database to accept connections, manually triggers its healthcheck, starts the app container, waits for the app health endpoint, and triggers its healthcheck. On systemd systems, the runtime check is skipped entirely (zero overhead). Both macros use a runtime shell check for /run/systemd/system, so the same Makefile works identically on all systems without parse-time conditionals. Applied to all compose-up targets: up, rebuild, rebuild-force, rebuild-force-db, rebuild-app, rebuild-app-force, test-integration, and test-env-up. Refs: https://github.com/containers/podman/pull/27033 --- Makefile | 70 +++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 600cd9f..e2e223f 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,48 @@ ifneq (,$(wildcard ./.env)) export endif +# On non-systemd systems (e.g., Void Linux with runit), podman healthchecks are +# never triggered automatically because podman relies on systemd timers. +# Additionally, podman-compose up -d blocks forever waiting for service_healthy +# conditions that never resolve. This macro: +# 1. Waits for the DB to accept connections, triggers its healthcheck +# 2. Starts the app container, waits for it, triggers its healthcheck +# On systemd systems, the runtime check skips everything. +# See: https://github.com/containers/podman/pull/27033 +define ensure_healthy + @if [ ! -d /run/systemd/system ]; then \ + echo "Non-systemd: waiting for database..."; \ + i=0; \ + until podman exec bookhoard_db pg_isready -U postgres >/dev/null 2>&1; do \ + i=$$((i+1)); if [ $$i -gt 30 ]; then echo " Database failed to start"; exit 1; fi; \ + sleep 2; \ + done; \ + podman healthcheck run bookhoard_db >/dev/null 2>&1 || true; \ + podman start bookhoard 2>/dev/null || true; \ + echo "Non-systemd: waiting for application..."; \ + i=0; \ + until podman exec bookhoard curl -sf http://localhost:8765/health >/dev/null 2>&1; do \ + i=$$((i+1)); if [ $$i -gt 30 ]; then echo " Application failed to start"; exit 1; fi; \ + sleep 2; \ + done; \ + podman healthcheck run bookhoard >/dev/null 2>&1 || true; \ + echo " Services healthy"; \ + fi +endef + +# podman-compose up -d blocks forever on non-systemd waiting for service_healthy. +# This macro runs compose with a timeout on non-systemd to let it create containers +# without hanging. Pass compose flags via the COMPOSE_UP_ARGS variable. +# Usage: $(call compose_up,--build --force-recreate) +# On systemd systems, the timeout is skipped. +define compose_up + @if [ -d /run/systemd/system ]; then \ + podman compose up $1 -d; \ + else \ + timeout 15 podman compose up $1 -d || true; \ + fi +endef + # Default target help: @echo "Available targets:" @@ -46,7 +88,8 @@ test-integration: @echo "Building test containers..." podman compose --profile tests build @echo "Starting application containers..." - podman compose up -d db app + $(call compose_up,db app) + $(ensure_healthy) @echo "Waiting for services to be healthy..." @until podman exec bookhoard_db pg_isready -U postgres > /dev/null 2>&1; do \ echo " Database not ready yet..."; \ @@ -71,27 +114,31 @@ test-all: test test-integration # Rebuild app container only (preserve DB, with cache) rebuild-app: @echo "Rebuilding app container (database stays running)..." - podman compose up --build --force-recreate -d app + $(call compose_up,--build --force-recreate app) + $(ensure_healthy) @echo "✓ App container rebuilt and restarted" # Rebuild app container only (preserve DB, no cache) rebuild-app-force: @echo "Force rebuilding app container (database stays running, no cache)..." podman compose build --no-cache app - podman compose up --force-recreate -d app + $(call compose_up,--force-recreate app) + $(ensure_healthy) @echo "✓ App container rebuilt and restarted" # Rebuild all containers (preserve DB, with cache) rebuild: @echo "Rebuilding all containers (database preserved)..." - podman compose up --build --force-recreate -d + $(call compose_up,--build --force-recreate) + $(ensure_healthy) @echo "✓ All containers rebuilt and restarted" # Rebuild all containers (preserve DB, no cache) rebuild-force: @echo "Force rebuilding all containers (database preserved, no cache)..." podman compose build --no-cache - podman compose up --force-recreate -d + $(call compose_up,--force-recreate) + $(ensure_healthy) @echo "✓ All containers rebuilt and restarted" # Rebuild all containers (remove DB, no cache) @@ -99,7 +146,8 @@ rebuild-force-db: @echo "Force rebuilding all containers (database will be DELETED, no cache)..." podman compose down -v podman compose build --no-cache - podman compose up --force-recreate -d + $(call compose_up,--force-recreate) + $(ensure_healthy) @echo "✓ All containers rebuilt and restarted" # Stop and remove containers @@ -108,7 +156,8 @@ clean: # Quick start (if already built) up: - podman compose up -d + $(compose_up) + $(ensure_healthy) # Stop all containers (alias for clean) down: @@ -131,7 +180,12 @@ logs: # Start containers with test mode enabled for manual testing test-env-up: @echo "Starting containers with test mode enabled..." - TEST_MODE=true RATE_LIMIT_ENABLED=false REQUESTS_PER_MINUTE=1000 podman compose up --build --force-recreate -d + @if [ -d /run/systemd/system ]; then \ + TEST_MODE=true RATE_LIMIT_ENABLED=false REQUESTS_PER_MINUTE=1000 podman compose up --build --force-recreate -d; \ + else \ + TEST_MODE=true RATE_LIMIT_ENABLED=false REQUESTS_PER_MINUTE=1000 timeout 15 podman compose up --build --force-recreate -d || true; \ + fi + $(ensure_healthy) @echo "Waiting for services to be ready..." @until podman exec bookhoard_db pg_isready -U postgres > /dev/null 2>&1; do sleep 1; done @until podman exec bookhoard curl -sf http://localhost:8765/health > /dev/null 2>&1; do sleep 1; done