From 4dd0a5275f3eff8e56c404ca5e46ba34ce8b979c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 22:07:54 +0200 Subject: [PATCH] ops: linux.md server runbook (access, conventions, hbapp user, status, next steps); start-from-scratch (only Forgejo migrates) --- linux.md | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 linux.md diff --git a/linux.md b/linux.md new file mode 100644 index 0000000..8d50153 --- /dev/null +++ b/linux.md @@ -0,0 +1,82 @@ +# CannaBuddy server runbook (`cannabuddy.eu` / 181.214.99.11) + +Operational guide for the new production VPS. Secrets live in +`passwords.md` (this private repo) — this file is the *how*, not the *what*. +**Decision: start the new cloud from scratch** — NO migration of +DB/users/devices/measurements. Members re-register, the board re-flashes. +**Only Forgejo + its data migrates** from the old Netcup box. + +--- + +## Access (SSH is key-only — password login disabled) +- **Claude (automation):** `ssh -i ~/.ssh/id_ed25519_cannabuddy root@181.214.99.11` +- **KJ desktop:** `ssh root@181.214.99.11` (default key `~/.ssh/id_ed25519`) +- **KJ Termius:** import `~/.ssh/id_ed25519`, host `181.214.99.11` user `root` +- **Fallback if locked out:** the VPS provider's web console (password still set, but SSH password auth is OFF). +- Pipe local scripts to the box without quoting pain: `ssh … 'bash -s' < script.sh` + +## The box +- Debian 12 (bookworm), 4 vCPU (Ryzen 9 7950X), 8 GB RAM, ~99 GB disk. +- IPv4 `181.214.99.11`, IPv6 `2a0f:5707:aaf1:210c::1`. Domain in Cloudflare (proxied = WAF). + +## Users / who runs what (proper-Linux: nothing app-level as root) +| Service | runs as | unit | +|---|---|---| +| Caddy (reverse proxy/TLS) | `caddy` | `systemctl … caddy` | +| PostgreSQL/TimescaleDB | `postgres` | `systemctl … postgresql` | +| Mosquitto (MQTT) | `mosquitto` | `systemctl … mosquitto` | +| **The app (Astro SSR + bridge)** | **`hbapp`** (uid 1000, home `/opt/cannabuddy`, login shell for setup) | **pm2** (to become a systemd unit via `pm2 startup systemd -u hbapp`) | +- Run app commands as the service user: `sudo -u hbapp -H bash -lc '…'`. +- App code will live in `/opt/cannabuddy/cloud` (the cloud repo checkout). + +## Installed + where +- **Caddy 2.11** (apt, cloudsmith repo) — config `/etc/caddy/Caddyfile` (mirror in `deploy/Caddyfile` here). HTTP/1+2+3, security headers, static CF Origin cert. +- **Node 22 + pm2 + pnpm** (NodeSource apt; `corepack enable` gives pnpm). +- **TimescaleDB 16** (2.27.1) on PostgreSQL 16 (PGDG apt). DB `cannabuddy`, role `hbcloud`. Tuned by `timescaledb-tune`. Listens localhost only. +- **Mosquitto 2.0.11** (apt) — **stopped** until `mosquitto-go-auth` (Postgres backend) is configured (localhost WS listener `:8083`, plain `:1883` for the local bridge). +- **Hardening:** `ufw` (allow 22, 80, 443 tcp + 443 udp; default-deny), `fail2ban` (sshd), `unattended-upgrades`. + +## TLS +- Cloudflare **Origin Certificate** at `/etc/ssl/cannabuddy/origin.{crt,key}` — dir `750 root:caddy`, key `640`, cert `644`. Valid to **2041, no renewal**. +- Cloudflare SSL/TLS mode must be **Full (strict)**. +- Gotcha learned: the cert *dir* must be group-`caddy` + traversable (`750`), not just the files. + +## Domains / Caddy routes (all behind Cloudflare proxy) +| Host | → | +|---|---| +| `cannabuddy.eu`, `www` | static landing `/var/www/cannabuddy-landing` | +| `app.cannabuddy.eu` | Astro SSR `127.0.0.1:7502` (+ `/mqtt` → Mosquitto WS `:8083`) | +| `admin.cannabuddy.eu` | same app `:7502` (lock behind **Cloudflare Access** later) | +| `git.cannabuddy.eu` | Forgejo `:3000` | +| `status.cannabuddy.eu` | Uptime Kuma `:3001` | +- Tenants are resolved by **login** (pooled DB + `tenant_id`), NOT by subdomain. + +## Secrets / config +- `/etc/cannabuddy/cloud.env` (root:hbapp `640`) — holds `DATABASE_URL` (+ to add: `BETTER_AUTH_SECRET`, SendKit key, R2 creds, MQTT auth secret). pm2 ecosystem loads it. +- All canonical secret values: `passwords.md` (this repo). + +## Current status (2026-05-30) +**Live:** hardening · Caddy (HTTPS, landing at https://cannabuddy.eu) · TimescaleDB DB+role · Mosquitto installed · `hbapp` user. `app.` 502s until the app deploys. + +## NEXT steps (resume here) +1. **Deploy app as `hbapp`:** clone cloud repo → `/opt/cannabuddy/cloud`, `pnpm install`, write env (DATABASE_URL + `openssl rand -hex 32` for BETTER_AUTH_SECRET + SendKit), `pnpm build`, run drizzle migrations, `pm2 start` (cluster) + `pm2 startup systemd -u hbapp` + `pm2 save`. → `app.cannabuddy.eu` green. +2. **New-architecture code (in the repo):** `tenant_id` on `measurements` + Timescale compression policy (≥7d); `device_credentials` table; `/api/provision` issues per-device opaque tokens + per-tenant device quota; swap Resend → **SendKit** (add SPF/DKIM/DMARC DNS). +3. **Mosquitto auth:** install/build `mosquitto-go-auth`, Postgres backend against `device_credentials`, ACL `tele//#` pub + `cmd//#` sub; enable WS listener `127.0.0.1:8083`; start mosquitto. +4. **FW relay/bridge (as hbapp, pm2):** subscribe `tele/#` → insert into TimescaleDB stamping `tenant_id`; publish `cmd//ota` + `cmd//factory-reset`. +5. **Forgejo:** install on box → copy data dir + DB from Netcup → `git.cannabuddy.eu`; verify; then decommission old. +6. **Uptime Kuma** → `status.cannabuddy.eu`. **Backups:** nightly `pg_dump` → R2 (EU endpoint) + tested restore. +7. **Validate E2E** with a test board → tag **v1.0.0** → re-flash the real board + give the 2 members the new link. + +## Handy commands +``` +# service status +systemctl status caddy postgresql mosquitto --no-pager +# reload caddy after editing /etc/caddy/Caddyfile +caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile && systemctl reload caddy +# psql +sudo -u postgres psql -d cannabuddy +# run as the app user +sudo -u hbapp -H bash -lc 'cd /opt/cannabuddy/cloud && pm2 ls' +# firewall +ufw status verbose +```