ops: linux.md server runbook (access, conventions, hbapp user, status, next steps); start-from-scratch (only Forgejo migrates)

This commit is contained in:
Claude 2026-05-30 22:07:54 +02:00
parent bbcd8ffe41
commit 4dd0a5275f

82
linux.md Normal file
View file

@ -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/<mac>/#` pub + `cmd/<mac>/#` 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/<mac>/ota` + `cmd/<mac>/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
```