From d8063c90f0f33f9ba410ced45ecc46cc05fcbc3a Mon Sep 17 00:00:00 2001 From: Guido X Jansen Date: Mon, 16 Feb 2026 14:01:27 +0100 Subject: [PATCH] fix(ci): use Docker healthcheck status instead of curl for deploy verification (#26) * docs(readme): document image tag convention (edge vs latest) Add Image Tags section explaining :latest (stable releases), :edge (staging builds from main), semver tags, and per-build staging tags. Helps self-hosters pick the right image version. * fix(ci): use Docker healthcheck status instead of curl for deploy verification The staging VPS doesn't expose API/Web ports to the host (only Caddy ports 80/443 are exposed). The health check was curling 127.0.0.1:3000 which is unreachable, causing every deploy to fail and trigger rollback. Now uses `docker inspect` to check Docker's own healthcheck status, which works regardless of port exposure. Also increases API timeout to 90s to account for the healthcheck start-period (30s) and includes container logs in failure output for easier debugging. --- .github/workflows/deploy-staging.yml | 37 ++++++++++++++++++---------- README.md | 23 +++++++++++++++++ infrastructure/staging.md | 2 +- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 4093d88..0cc7e1f 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -225,28 +225,39 @@ jobs: cd ${{ env.DEPLOY_PATH }} echo "Checking health endpoints..." - # Wait up to 60s for API health - for i in $(seq 1 12); do - API_STATUS=$(curl -sf -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/api/health 2>/dev/null || echo "000") - if [ "$API_STATUS" = "200" ]; then - echo "API health: OK (200)" + # Wait up to 90s for API to become healthy (Docker healthcheck: start-period 30s + interval 30s) + # Ports are not exposed to host, so we check Docker's own healthcheck status + for i in $(seq 1 18); do + API_HEALTH=$(docker inspect --format='{{.State.Health.Status}}' $(docker compose ${{ env.COMPOSE_FILES }} ps -q barazo-api 2>/dev/null) 2>/dev/null || echo "missing") + if [ "$API_HEALTH" = "healthy" ]; then + echo "API health: OK (healthy)" break fi - echo "API health: $API_STATUS (attempt $i/12)" + echo "API health: $API_HEALTH (attempt $i/18)" sleep 5 done - if [ "$API_STATUS" != "200" ]; then - echo "::error::API health check failed after 60s" + if [ "$API_HEALTH" != "healthy" ]; then + echo "::error::API health check failed after 90s (status: $API_HEALTH)" + # Show recent logs for debugging + docker compose ${{ env.COMPOSE_FILES }} logs --tail=20 barazo-api 2>&1 || true exit 1 fi # Check web health - WEB_STATUS=$(curl -sf -o /dev/null -w "%{http_code}" http://127.0.0.1:3001/api/health 2>/dev/null || echo "000") - if [ "$WEB_STATUS" = "200" ]; then - echo "Web health: OK (200)" - else - echo "::error::Web health check failed: $WEB_STATUS" + for i in $(seq 1 12); do + WEB_HEALTH=$(docker inspect --format='{{.State.Health.Status}}' $(docker compose ${{ env.COMPOSE_FILES }} ps -q barazo-web 2>/dev/null) 2>/dev/null || echo "missing") + if [ "$WEB_HEALTH" = "healthy" ]; then + echo "Web health: OK (healthy)" + break + fi + echo "Web health: $WEB_HEALTH (attempt $i/12)" + sleep 5 + done + + if [ "$WEB_HEALTH" != "healthy" ]; then + echo "::error::Web health check failed after 60s (status: $WEB_HEALTH)" + docker compose ${{ env.COMPOSE_FILES }} logs --tail=20 barazo-web 2>&1 || true exit 1 fi diff --git a/README.md b/README.md index b4dcae2..0510567 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,29 @@ Production uses two-network segmentation: PostgreSQL and Valkey sit on the `back --- +## Image Tags + +Barazo API and Web images are published to [GitHub Container Registry](https://github.com/orgs/barazo-forum/packages) (`ghcr.io/barazo-forum/*`). + +| Tag | Meaning | When to use | +|-----|---------|-------------| +| `:latest` | Latest stable release | **Production.** Self-hosters should pin to this or a specific version. | +| `:1.0.0`, `:1.0`, `:1` | Semver release tags | **Production.** Pin to a major or minor version for controlled upgrades. | +| `:edge` | Latest build from `main` | **Staging/testing only.** Rebuilt on every push to `main`. May contain breaking changes. | +| `:staging-{N}` | Immutable per-build tag | **Debugging.** Trace a specific staging deploy to its build number. | +| `:sha-{hash}` | Git commit SHA | **Debugging.** Trace an image to its exact source commit. | + +**For self-hosters:** Use `:latest` or pin to a semver tag in your `.env`: + +```bash +BARAZO_API_VERSION=1.0.0 +BARAZO_WEB_VERSION=1.0.0 +``` + +The production `docker-compose.yml` reads these variables (defaults to `latest` if unset). + +--- + ## Deployment Modes **Development:** diff --git a/infrastructure/staging.md b/infrastructure/staging.md index 46e7676..9edbaca 100644 --- a/infrastructure/staging.md +++ b/infrastructure/staging.md @@ -23,7 +23,7 @@ Push to main (api or web) 3. docker compose pull (api + web only) 4. docker compose up -d 5. Watch logs for 30s -- grep crash patterns - 6. Curl health endpoints (API :3000, Web :3001) + 6. Check Docker healthcheck status (API + Web) 7. If unhealthy: auto-rollback to previous digests ``` -- 2.51.2