Files
route-commerce/deploy/README.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

114 lines
5.0 KiB
Markdown

# deploy/ — single-server PostgREST + Next.js production deploy
Idempotent, log-everything, port-juggling deploy for a homelab or small
production box where many services compete for ports.
## What you get
| File | Purpose |
|---|---|
| `deploy.sh` | The main script. Runs cleanup, port selection, build, deploy, nginx render, healthcheck, persist. |
| `docker-compose.yml` | The stack: `postgrest` and `nextjs`. Reads `POSTGREST_HOST_PORT` / `NEXTJS_HOST_PORT` / `NEXT_PUBLIC_API_URL` from `.env.production`. |
| `Dockerfile.nextjs` | Multi-stage Next.js image. Uses the host's pre-built `.next/`, runs as non-root. |
| `nginx.conf.template` | Rendered to `/etc/nginx/sites-available/prod-app.conf` on every deploy. `/api/*` → PostgREST, else → Next.js. |
| `.env.production.example` | Sample env file. `deploy.sh` writes the first three lines and preserves everything else. |
| `healthcheck.sh` | Standalone, callable from cron / monitoring. Exits with the failure count. |
| `Makefile` | `make deploy`, `make status`, `make health`, `make rollback`, etc. |
| `GITEA_SETUP.md` | How to wire this into Gitea (webhook vs Actions runner). |
## Files written at runtime (workspace root)
| File | Written by | Read by | Purpose |
|---|---|---|---|
| `.deploy.lock` | `deploy.sh` (flock) | `deploy.sh` | Prevents concurrent deploys. |
| `deploy.log` | `deploy.sh` (tee) | humans | Append-only log with timestamps and section headers. |
| `.postgrest-port` | `deploy.sh` (atomic write) | `deploy.sh`, `healthcheck.sh` | Current prod port for the PostgREST API. |
| `.nextjs-port` | `deploy.sh` (atomic write) | `deploy.sh`, `healthcheck.sh` | Current prod port for the Next.js frontend. |
| `.env.production` | `deploy.sh` (preserves secrets) | `docker compose`, runtime | Ports + your secrets. |
## Quick start
```bash
# 1. Populate secrets
cp deploy/.env.production.example .env.production
$EDITOR .env.production
chmod 600 .env.production
# 2. First deploy
./deploy/deploy.sh
# 3. Day-to-day
make status
make health
make logs
```
## The contract
After every successful deploy:
- The PostgREST container is reachable on the port stored in
`.postgrest-port` (the first free port in `[3011..30200]`).
- The Next.js container is reachable on the port stored in `.nextjs-port`
(the next free port in the same range).
- `nginx` reverse-proxies `/api/*` to the PostgREST port and everything
else to the Next.js port.
- `.env.production` is updated to match.
- A previous failed deploy does NOT clobber the working `.postgrest-port`
the new value is only committed after the healthcheck passes.
## Overriding defaults
Every variable in the top of `deploy.sh` can be overridden via the
environment:
```bash
NEXT_PUBLIC_API_URL=https://app.example.com/api \
PROJECT_NAME=prod-app-blue \
PORT_RANGE_START=4000 PORT_RANGE_END=4200 \
./deploy/deploy.sh
```
Notable variables:
- `WORKSPACE` — root of the repo (default: parent of `deploy/`).
- `COMPOSE_FILE` — path to the compose file.
- `NGINX_TEMPLATE` / `NGINX_RENDERED` / `NGINX_LINK` — nginx template and
output paths.
- `POSTGREST_PORT_FILE` / `NEXTJS_PORT_FILE` — port tracker locations.
- `DEV_PORT` — port the dev stack uses (default 3001), freed on every run.
- `HEALTHCHECK_TIMEOUT` / `HEALTHCHECK_INTERVAL` — how long to wait.
- `PRUNE_IMAGES` — set to `0` to skip `docker image prune -f`.
- `NEXT_PUBLIC_API_URL` — the public URL the browser uses. Default
`http://localhost:<port>` is fine for LAN-only dev. **For production
with a real domain, set this to `https://yourdomain.com/api` (or similar)
before running deploy.**
## Why these choices
- **`flock` over `.deploy.lock` files with manual `mkdir`-style locking.**
Kernel-level, releases on process death (including SIGKILLs that don't
leave a stale lock file), and trivially scriptable.
- **Atomic file writes for `.postgrest-port`.** The reader always sees
either the old value or the new value, never a half-written one. This
matters because `healthcheck.sh` (cron, monitoring) reads this file
concurrently with deploys.
- **Port files are committed to disk only AFTER the healthcheck passes.**
A failed deploy leaves the previous port in place, so the rollback path
is "use the port that was working before."
- **`ss -tlnH` over `lsof` / `netstat`.** `ss` is in `iproute2` on every
modern distro, doesn't need root for unprivileged ports, and is
trivially scriptable. The output covers both IPv4 and IPv6 listeners.
- **Stale-port guard.** If `.postgrest-port` points to a port nothing is
listening on (e.g., a manual cleanup left the file), we still tear down
the compose project (cheap) but we don't `kill` arbitrary PIDs holding
that port — someone else might be using it.
- **`systemctl reload nginx` (not `restart`).** Zero-downtime config
changes; the binary keeps serving existing connections.
- **`.env.production` is owned by us but we preserve unknown lines.** A
user's secrets stay where they put them, even when we rewrite the port
block on every deploy.
See `GITEA_SETUP.md` for the two ways to wire this into your Gitea
instance.