Files
route-commerce/deploy/GITEA_SETUP.md
T
tyler cca4bda1fc
Deploy to route.crispygoat.com / deploy (push) Failing after 15s
Build / build (push) Has been cancelled
feat(deploy): add self-hosted homelab deploy toolkit
- deploy/deploy.sh: idempotent deploy script with dynamic port
  allocation (3011..30200), flock-based concurrency, atomic
  .postgrest-port/.nextjs-port writes, port cleanup of the previous
  deploy + dev stack, nginx config rendering+reload, healthchecks
  with rollback, optional image pruning
- deploy/docker-compose.yml + Dockerfile.nextjs: example stack
  consuming ${POSTGREST_HOST_PORT} / ${NEXTJS_HOST_PORT} (kept as
  reference; the repo's root docker-compose.yml is the source of
  truth for the actual production stack)
- deploy/nginx.conf.template: /api/* -> PostgREST, /* -> Next.js
- deploy/.env.production.example: managed port block + preserved secrets
- deploy/healthcheck.sh: standalone health probe (cron-friendly)
- deploy/Makefile: deploy/status/health/logs/down/rollback targets
- deploy/GITEA_SETUP.md: webhook vs Actions runner instructions
- deploy/README.md + deploy/.gitignore

Note: .gitea/workflows/deploy.yml was deliberately not added — the
existing workflow at that path on Gitea main is the source of truth
and is left untouched.
2026-06-06 00:47:44 +00:00

4.4 KiB

Wiring deploy.sh into Gitea

Two practical patterns, both supported:

Option A — Gitea webhook (push event → cURL on the server)

Simplest. A push to main causes Gitea to POST to a small endpoint on the homelab server, which then runs deploy.sh.

A.1. Add a webhook secret in Gitea

  • Repository → Settings → Webhooks → Add Webhook → Gitea
  • Target URL: https://deploy.example.com/hooks/gitea (your reverse-proxied endpoint that runs the script)
  • HTTP method: POST
  • POST content type: application/json
  • Secret: a long random string
  • Trigger on: "Push events"
  • Branch filter: main (or gitea-sync)
  • Save and note the secret.

A.2. Drop a tiny receiver on the homelab server

A 5-line systemd-timer-friendly shell receiver is enough. For example:

#!/usr/bin/env bash
# /usr/local/bin/gitea-deploy-webhook
set -euo pipefail
LOG=/var/log/gitea-deploy.log
echo "[$(date -Iseconds)] trigger" >> "$LOG"
sudo -u deploy /srv/app/deploy/deploy.sh >> "$LOG" 2>&1

Expose it via a separate server { ... } block in nginx that listens on something obscure and allow/denys only Gitea's source IPs. Or put it behind a Cloudflare Tunnel / Tailscale Funnel. The exact exposure model is up to you.

A.3. Hardening the secret

If you want the receiver to verify Gitea's HMAC, add this check (works with GITEA_WEBHOOK_SECRET matching what you set in the UI):

SECRET='paste-your-secret-here'
sig=$(printf '%s' "$HTTP_RAW_BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | xxd -p -c 256)
expected=$(printf 'sha256=%s' "$sig")
[[ "$HTTP_X_GITEA_SIGNATURE" == "$expected" ]] || { echo "bad sig"; exit 1; }

(Your receiver framework — webhook, socat, caddy plugin — handles header propagation differently; adapt accordingly.)


Option B — Gitea Actions runner (self-hosted)

The Gitea-native CI path. Most flexible: only deploy when the runner is on the homelab.

B.1. Register a self-hosted runner

On the homelab, follow https://docs.gitea.com/usage/actions/act_runner and register a runner labeled self-hosted,homelab.

B.2. Workflow file (already in the repo)

The workflow lives at .gitea/workflows/deploy.yml in the repo root. It triggers on push to main / gitea-sync and calls ./deploy/deploy.sh. Highlights:

  • on.push.paths filter — only deploys when source, deploy config, or the workflow itself changes. Drop the block to deploy on every commit.
  • workflow_dispatch — manual trigger from the Gitea UI with optional inputs for api_url, project_name, skip_prune. Useful for blue/green deploys (project_name: prod-app-green).
  • concurrency.group — at most one deploy runs at a time (the script also has its own flock).
  • runs-on: [self-hosted, homelab] — must match the labels you gave the runner.
  • Preflight step — verifies docker, docker compose, flock, ss, curl exist and the required files are present, BEFORE the script takes its lock.
  • Post-deploy healthcheck.sh --nginx — independent smoke test in addition to the script's own healthcheck.
  • Failure annotation — tail of deploy.log printed on failure so the Actions log shows what went wrong without a separate SSH session.

B.3. Optional: path filter tweaks

To deploy on docs-only commits, drop the paths: block under on.push. To deploy on PR merges only, replace the trigger with:

on:
  push:
    branches: [main]

B.4. Required secrets

  • GITEA_TOKEN (a personal access token with write:repository scope) — only needed for the "annotate the commit" step (which posts the new ports as a commit comment). The rest of the workflow works without any secrets.

The deploy itself reads .env.production from the workspace, so no app secrets need to be plumbed through Actions (the runner has filesystem access to /srv/app/).


Comparison

Webhook (A) Actions (B)
Setup cost Trivial Moderate (runner install)
Logging deploy.log only Gitea Actions UI + deploy.log
Conditional triggers Receiver must parse the body YAML in the workflow
Multi-repo One webhook per repo One workflow per repo
Source of truth Webhook delivery Runner job history

For a single homelab repo, Option B is recommended — you get the Actions UI for free and the runner already lives on the box, so there's no extra network surface area to secure.