ops/deploy.py ships each service's build context to the remote Docker daemon's /build endpoint and pushes docker-compose.prod.yml as a Portainer stack — there is no docker CLI in WSL and Unraid's SSH is closed. The prod compose file drops the dev bind mounts and uvicorn --reload, publishes only the frontend port (8000 is taken by Portainer's Edge tunnel), and pins bms_net to 172.31.42.0/24 because the host's default address pools are fully subnetted. Mosquitto's config is baked into an image since the repo is not checked out on the host. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.5 KiB
5.5 KiB
BMS — memory
DCIM demo platform: Next.js 16 frontend, FastAPI backend, TimescaleDB, Mosquitto MQTT, and Python simulator bots that publish fake data-centre telemetry.
Live at http://192.168.1.249:5646/dashboard (Portainer stack bms on Unraid).
Decisions & rationale
- Auth is disabled (demo mode). The repo shipped half-converted:
proxy.tsalready said "auth is disabled for demo mode", butapp/layout.tsxstill wrapped the tree in<ClerkProvider>andtopbar.tsxused<UserButton>.@clerk/nextjsv7 failsnext buildwithout a real publishable key, so the frontend image could not build at all. Finished the conversion instead of buying Clerk keys: removedClerkProvider/UserButton, deleted thesign-in/sign-uproute groups, and dropped@clerk/nextjsfrompackage.json. The service is LAN-only and reachable remotely over Tailscale, so the tailnet is the auth boundary. - Deploy via the Portainer API, not compose-on-host. No
dockerCLI in WSL and Unraid's SSH (port 22) is closed, soops/deploy.pytars each service's build context, POSTs it to the remote daemon's/buildendpoint, then pushesdocker-compose.prod.ymlas a Portainer stack. Same pattern astoknmtr/ops/deploy.py. - Separate
docker-compose.prod.yml. The dev compose file bind-mounts source into the containers and runsuvicorn --reload— wrong for a long-running deploy, and impossible anyway since the repo is not checked out on the Unraid host. The prod file references prebuilt image tags, sets env inline (no.envfiles on the host), and publishes only the frontend's port. - Mosquitto config is baked into an image (
infra/mosquitto/Dockerfile) rather than bind-mounted, for the same reason: no repo on the host. bms_netpins subnet 172.31.42.0/24. Docker's default address pools on that host are fully subnetted — 24 existing stacks have claimed every172.17-172.31/16and192.168.x/20slot, so auto-allocation fails with "all predefined address pools have been fully subnetted". An explicit subnet bypasses the allocator without disturbing other stacks. Only other 172.31 tenant iswg0on 172.31.200.0/24.- Postgres data bind-mounts to
/mnt/user/appdata/bms/db, not a named volume, so a growing timeseries DB lands on the array instead of filling Unraid's fixed-sizedocker.img. pnpm-workspace.yamlmust be copied in the frontend Dockerfile's deps stage. It carries the build-script allowlist; without it pnpm aborts withERR_PNPM_IGNORED_BUILDS. The file now declares bothallowBuilds(pnpm 11 spelling) andignoredBuiltDependencies(pnpm 10).sharp,unrs-resolverandmsware all declined — the app has nonext/image, so sharp is dead weight.packageManager: pnpm@11.20.0is pinned so a future pnpm release cannot silently change this again.
Open questions / TODOs
- Postgres uses the repo's default credentials (
dcim/dcim_pass). Fine while the DB has no published host port and the box is LAN-only, but change both the compose env andDATABASE_URLtogether if the port is ever exposed. - The simulator floods logs with
mqtt WARNING There are N pending publish calls.— it publishes faster than aiomqtt drains. Cosmetic, but it makesdocker logsnear-useless and grows the log file. Worth batching or throttling the bots. backend/api/routes/ws.pyexists but nothing in the frontend opens a WebSocket (the UI polls every 15 s). Either wire it up or drop it. Note: Next.js rewrites do not proxy WS upgrades, so a WS client would need to reach the backend directly.- No host port is published for Postgres or MQTT. Add a
ports:entry todocker-compose.prod.ymlif direct DB access or external MQTT publishing is ever needed. - No reverse-proxy entry yet. Add an Nginx Proxy Manager host pointing at
:5646if this should be reachable by name rather than IP:port.
Session log
2026-08-04
- Cloned the Forgejo repo into
~/claude/projects/BMSand deployed it to the Unraid host. - Finished the demo-mode auth conversion (see Decisions) so the frontend image could build;
regenerated
pnpm-lock.yamlwithout@clerk/nextjs. Verifiednext buildlocally — all 18 routes prerender. - Hit and fixed three deploy blockers in order: (1) the frontend Dockerfile never copied
pnpm-workspace.yaml, so pnpm's ignored-builds allowlist was invisible inside the build; (2) pnpm 11 renamed that setting toallowBuildsand ignoresignoredBuiltDependencies; (3) the Unraid daemon's Docker address pools were exhausted, so the stack network could not be created. - Backend's host port 8000 deliberately not published — Portainer's Edge tunnel already owns 8000 on that host. Ports 1883/5433/5646 were free; only 5646 is published.
- Added
ops/deploy.py,docker-compose.prod.yml,infra/mosquitto/Dockerfile; updated the README (demo-mode note, Unraid deploy section) and both.envexamples. - Verified live: all 5 containers healthy, 30 min of seeded history present, telemetry flowing, alarm engine firing (55 active), dashboard screenshotted and rendering correctly.
External references
- Repo: https://git.rdx4.com/megaproxy/BMS
- Live dashboard: http://192.168.1.249:5646/dashboard
- API docs (Swagger): http://192.168.1.249:5646/api/backend/docs
- Portainer (stack
bms, endpoint id 3): http://192.168.1.249:9000 — token at~/.portainer-token - Postgres data on the host:
/mnt/user/appdata/bms/db - Deploy pattern copied from:
~/claude/projects/toknmtr/ops/deploy.py