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.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
#!/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`/`deny`s 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):
|
||||
|
||||
```bash
|
||||
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](../.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:
|
||||
|
||||
```yaml
|
||||
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.
|
||||
Reference in New Issue
Block a user