Deployment — tools.spboucher.ai on MacLustr
Topology
browser ──HTTPS──▶ BHS64 (OVH Beauharnois, 51.161.112.61) Caddy, Let's Encrypt
└─ WireGuard wg0 10.67.0.1 ──▶ M2M32c wg1 10.67.0.20 : 8540 (Next.js, PM2 `tools-web`)
├─ PM2 `tools-worker` (jobs, sweeps, backups)
└─ PostgreSQL 17 + pgvector (localhost:5432, db tools_spboucher)
DNS: GoDaddy A tools.spboucher.ai → 51.161.112.61 (pre-existing, verified 2026-11-02)
Orchestration: M1M32 `mld` (manifest ~/dispatch/apps/tools-spboucher-ai.json), registry, heal every 5 min, route managementOne-time node preparation (done 2026-11-02)
ssh M2M32c
export PATH=/opt/homebrew/opt/postgresql@17/bin:/opt/homebrew/bin:$PATH
createdb tools_spboucher
# roles with generated passwords (printed once; copied into .env)
DATABASE_URL_ADMIN=postgres://localhost:5432/tools_spboucher pnpm db:roles
mkdir -p ~/apps/tools-spboucher-ai ~/backups/tools-spboucher-ai && chmod 700 ~/backups/tools-spboucher-ai
# .env: copy ~/Desktop/Cluster/secrets/tools-spboucher-ai/.env → ~/apps/tools-spboucher-ai/.env (mode 600)Secrets 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.
Manifest (infra/mld/tools-spboucher-ai.json → M1M32:~/dispatch/apps/)
PM2 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.
Release procedure (remote-first after first deploy)
First deploy (creates the app):
mld stage ~/Desktop/Cluster/projets/tools-spboucher-ai tools-spboucher-ai
mld deploy tools-spboucher-ai --node M2M32c # sync, hooks, PM2, local health, Caddy route, public health
mld adopt tools-spboucher-ai # node dir becomes a git clone, origin = spbgit tools-spboucher-ai.git
# M2M32c lacked the `gitsrv` SSH alias (→ 192.168.2.72); it was added to ~/.ssh/config before the first push.Subsequent changes (on the node):
ssh M2M32c "cd ~/apps/tools-spboucher-ai && git status --short && git pull" # or edit on the node
ssh M2M32c "cd ~/apps/tools-spboucher-ai && pnpm install --frozen-lockfile && pnpm build && pnpm db:migrate && pm2 restart tools-web tools-worker"
curl -fsS https://tools.spboucher.ai/api/health
ssh M2M32c "cd ~/apps/tools-spboucher-ai && git add -A && git commit -m '…' && git push origin main"Migrations 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).
Rollback
ssh M2M32c "cd ~/apps/tools-spboucher-ai && git log --oneline -5"
ssh M2M32c "cd ~/apps/tools-spboucher-ai && git checkout <previous-sha> && pnpm install --frozen-lockfile && pnpm build && pm2 restart tools-web tools-worker"Schema 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.
Backups
- Nightly 04:20 (worker) →
~/backups/tools-spboucher-ai/tools-<stamp>.dump.enc(AES-256-CBC, PBKDF2) → rsync toM3U96b:~/backups/tools-spboucher-ai/; retention 14. - Manual:
pnpm backup. Drill:pnpm restore:drill(createstools_restore_drill_*, restores, compares counts + content hash, drops it). - 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.
Operations
| Task | Command |
|---|---|
| Where is it / health | mld status --live, curl https://tools.spboucher.ai/api/health |
| Logs | mld logs tools-spboucher-ai or ssh M2M32c pm2 logs tools-web --lines 100 |
| Restart | mld restart tools-spboucher-ai |
| Move to another node | mld move tools-spboucher-ai --to <node> (needs Postgres 17 + pgvector there; restore DB from backup first) |
| Route check | mld tunnel status (expect tools.spboucher.ai → 10.67.0.20:8540 (M2M32c)) |
| Re-run link sweep | Admin → Jobs → Link sweep, or pnpm links:check on the node |
| Content update | edit content/, pnpm content:validate, pnpm content:import --publish on the node, commit |
Reverse-proxy trust
Caddy 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).