# 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.
