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
This commit is contained in:
2026-05-23 23:48:06 -04:00
parent 666b72c4fd
commit 604a2458e9
+62 -8
View File
@@ -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