cca4bda1fc
- 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.
114 lines
5.0 KiB
Markdown
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.
|