spb/tools-spboucher-ai
Public
1# Deployment — tools.spboucher.ai on MacLustr23## Topology4```5browser ──HTTPS──▶ BHS64 (OVH Beauharnois, 51.161.112.61) Caddy, Let's Encrypt6 └─ WireGuard wg0 10.67.0.1 ──▶ M2M32c wg1 10.67.0.20 : 8540 (Next.js, PM2 `tools-web`)7 ├─ PM2 `tools-worker` (jobs, sweeps, backups)8 └─ PostgreSQL 17 + pgvector (localhost:5432, db tools_spboucher)9DNS: GoDaddy A tools.spboucher.ai → 51.161.112.61 (pre-existing, verified 2026-11-02)10Orchestration: M1M32 `mld` (manifest ~/dispatch/apps/tools-spboucher-ai.json), registry, heal every 5 min, route management11```1213## One-time node preparation (done 2026-11-02)14```bash15ssh M2M32c16export PATH=/opt/homebrew/opt/postgresql@17/bin:/opt/homebrew/bin:$PATH17createdb tools_spboucher18# roles with generated passwords (printed once; copied into .env)19DATABASE_URL_ADMIN=postgres://localhost:5432/tools_spboucher pnpm db:roles20mkdir -p ~/apps/tools-spboucher-ai ~/backups/tools-spboucher-ai && chmod 700 ~/backups/tools-spboucher-ai21# .env: copy ~/Desktop/Cluster/secrets/tools-spboucher-ai/.env → ~/apps/tools-spboucher-ai/.env (mode 600)22```23Secrets in `.env`: `DATABASE_URL` (tools_app), `DATABASE_URL_PUBLIC` (tools_reader), `DATABASE_URL_ADMIN` (owner role for migrations/backups), `SESSION_SECRET`, `WEBAUTHN_RP_ID=tools.spboucher.ai`, `WEBAUTHN_ORIGIN=https://tools.spboucher.ai`, `APP_URL`, `OWNER_EMAIL`, `OWNER_INITIAL_PASSWORD` (changed at first login), `BACKUP_PASSPHRASE`, `BACKUP_DIR`, `BACKUP_REMOTE`, `AI_PROVIDER=none`.2425## Manifest (`infra/mld/tools-spboucher-ai.json` → `M1M32:~/dispatch/apps/`)26PM2 `tools-web` = `node node_modules/next/dist/bin/next start -p 8540 -H 0.0.0.0`; `tools-worker` = `node node_modules/tsx/dist/cli.mjs src/worker/index.ts`. Hooks (post_sync): require `.env` → `pnpm install --frozen-lockfile` → `pnpm db:migrate` + `pnpm content:import --publish` + `pnpm bootstrap` → `pnpm build` (static generation reads the database, so migrations run first). Placement pinned to M2M32c. `tunnel.domain = tools.spboucher.ai`.2728## Release procedure (remote-first after first deploy)29First deploy (creates the app):30```bash31mld stage ~/Desktop/Cluster/projets/tools-spboucher-ai tools-spboucher-ai32mld deploy tools-spboucher-ai --node M2M32c # sync, hooks, PM2, local health, Caddy route, public health33mld adopt tools-spboucher-ai # node dir becomes a git clone, origin = spbgit tools-spboucher-ai.git34# M2M32c lacked the `gitsrv` SSH alias (→ 192.168.2.72); it was added to ~/.ssh/config before the first push.35```36Subsequent changes (on the node):37```bash38ssh M2M32c "cd ~/apps/tools-spboucher-ai && git status --short && git pull" # or edit on the node39ssh M2M32c "cd ~/apps/tools-spboucher-ai && pnpm install --frozen-lockfile && pnpm build && pnpm db:migrate && pm2 restart tools-web tools-worker"40curl -fsS https://tools.spboucher.ai/api/health41ssh M2M32c "cd ~/apps/tools-spboucher-ai && git add -A && git commit -m '…' && git push origin main"42```43Migrations are additive and tracked by checksum; `pnpm build` happens before `pm2 restart`, so the previous build keeps serving until the switch (seconds of downtime at restart).4445## Rollback46```bash47ssh M2M32c "cd ~/apps/tools-spboucher-ai && git log --oneline -5"48ssh M2M32c "cd ~/apps/tools-spboucher-ai && git checkout <previous-sha> && pnpm install --frozen-lockfile && pnpm build && pm2 restart tools-web tools-worker"49```50Schema rollback: migrations never drop data columns; if a migration must be reverted, restore the latest backup into a scratch database (`pnpm restore:drill` proves the path) and copy rows back, or write a forward migration. The backup taken by `mld` before sync (`~/dispatch/stage`) and the nightly encrypted dumps are the recovery points.5152## Backups53- Nightly 04:20 (worker) → `~/backups/tools-spboucher-ai/tools-<stamp>.dump.enc` (AES-256-CBC, PBKDF2) → rsync to `M3U96b:~/backups/tools-spboucher-ai/`; retention 14.54- Manual: `pnpm backup`. Drill: `pnpm restore:drill` (creates `tools_restore_drill_*`, restores, compares counts + content hash, drops it).55- Restore for real: `openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 -in <file>.enc -out dump -pass env:TOOLS_BACKUP_PASS && pg_restore --clean --if-exists -d tools_spboucher dump`.5657## Operations58| Task | Command |59|---|---|60| Where is it / health | `mld status --live`, `curl https://tools.spboucher.ai/api/health` |61| Logs | `mld logs tools-spboucher-ai` or `ssh M2M32c pm2 logs tools-web --lines 100` |62| Restart | `mld restart tools-spboucher-ai` |63| Move to another node | `mld move tools-spboucher-ai --to <node>` (needs Postgres 17 + pgvector there; restore DB from backup first) |64| Route check | `mld tunnel status` (expect `tools.spboucher.ai → 10.67.0.20:8540 (M2M32c)`) |65| Re-run link sweep | Admin → Jobs → Link sweep, or `pnpm links:check` on the node |66| Content update | edit `content/`, `pnpm content:validate`, `pnpm content:import --publish` on the node, commit |6768## Reverse-proxy trust69Caddy forwards `X-Forwarded-For`/`X-Forwarded-Proto`; the app reads the first XFF hop for rate limiting and audit only (never for authorization). Cookies are `Secure` in production; `APP_URL`/`WEBAUTHN_ORIGIN` pin the canonical origin for CSRF and passkeys. Apex `spboucher.ai` is unaffected (no redirect added; other subdomains untouched).70