chore(deploy): add manifest, systemd units, nginx config, deploy.sh

Wires up the prod deployment per architecture-doc conventions:

- api → nikola.kosherinata.internal, loopback bind 127.0.0.1:42424
  (less-common port, registered with SELinux as http_port_t).
- worker → frootmig.kosherinata.internal, no listening port.
- web (static ui/dist + nginx server_name rob.tn) → nikola, with
  /api/* reverse-proxied to the loopback API.
- db → existing magrathea cluster via mTLS, hostname-baked DATABASE_URL
  rendered into /etc/moments/{api,worker}.env at deploy time.

Cert rotation: step-ca renews host certs every 24h; .path units watch
/etc/pki/tls/misc/<host>.pem and trigger systemctl restart of the
relevant service. Both binaries hold cert state in rustls and read
once at startup, so restart is the right reload semantics.

deploy.sh contract matches the architecture doc: positional env arg,
component list (or `all` / `default`), --dry-run support. Renders
config templates from `pass`, rsyncs over ssh+sudo, runs sysusers /
restorecon / semanage / systemctl / nginx -t idempotently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 20:17:17 +03:00
parent 7919a2d9ab
commit 110b523fd0
13 changed files with 602 additions and 2 deletions

View File

@@ -34,8 +34,38 @@ The API expects a Postgres reachable at `DATABASE_URL`. For magrathea, that's an
DATABASE_URL=postgres://localhost/moments cargo run -p moments-api
```
Migrations live in `crates/moments-data/migrations/` and run automatically on API startup.
Migrations live in `crates/moments-data/migrations/` and run automatically on worker startup. The API connects as `moments_ro` and never runs migrations — the worker (as `moments_rw`) is the schema owner.
## Deployment
See `asset/manifest.yml` and `script/deploy.sh`.
```sh
./script/deploy.sh prod all # api + worker + web
./script/deploy.sh prod api worker # subset
./script/deploy.sh prod default # api + web only (worker untouched)
./script/deploy.sh prod all --dry-run
```
Topology:
| Component | Host | Notes |
| --------- | --------------------------------- | --------------------------------------------- |
| api | `nikola.kosherinata.internal` | binds `127.0.0.1:42424`, fronted by local nginx |
| worker | `frootmig.kosherinata.internal` | no listening port; pollers only |
| web | `nikola.kosherinata.internal` | static `ui/dist/` under `/var/www/moments` |
| db | `magrathea.kosherinata.internal` | postgres mTLS, passwordless |
Postgres roles `moments_rw` and `moments_ro` must exist on the primary, with `pg_ident.conf` mappings in place for `nikola.kosherinata.internal``moments_ro` and `frootmig.kosherinata.internal``moments_rw`. See `asset/sql/bootstrap-moments.sql` and `asset/postgres/ident.conf.tmpl`.
Secrets resolved by `deploy.sh` via `pass`:
- `github.com/grenade/admin-token` — GitHub PAT for events + search APIs (worker only).
Optional, set if needed in `worker.env`: `GITEA_TOKEN`, `BUGZILLA_API_KEY`.
### DNS cutover
`rob.tn` currently resolves to GitHub Pages. After the first successful prod deploy:
1. Update Cloudflare DNS for `rob.tn` to the WAN IP that fronts `nikola` (unproxied — see architecture doc §11).
2. Confirm `curl -fsS https://rob.tn/api/v1/healthz` returns `ok`.
3. Add an archival notice to the top of [grenade-events-react/readme.md](https://github.com/grenade/grenade-events-react) pointing at this repo, and archive the GitHub repo.