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