plans: TRMNL plugins LXC (128) + middleware deploy pipeline
Pre-flight runbook for a dedicated trmnl LXC hosting self-hosted middleware for TRMNL e-ink plugins (TRMNL cloud polls /<plugin>/dashboard). First consumer: munich-home. Source repo dtoro/terminalito; this documents the fabric wiring (provision via homelab client add, Shape-B auto-deploy, SOPS secret, caddy + VPS traefik exposure). Status: Planned. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
107
plans/2026-06-24-trmnl-plugins-lxc.md
Normal file
107
plans/2026-06-24-trmnl-plugins-lxc.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# 2026-06-24 — TRMNL plugins LXC (128) + middleware deploy pipeline
|
||||
|
||||
## Goal
|
||||
|
||||
Stand up a dedicated LXC to host self-hosted **middleware for TRMNL e-ink plugins**.
|
||||
TRMNL cloud polls `https://trmnl.hubris.network/<plugin>/dashboard` every 15 min; the
|
||||
middleware fetches/shapes live data and returns JSON that TRMNL merges into the plugin's
|
||||
Liquid template. First consumer: the Munich Home Dashboard (`/munich-home/dashboard`).
|
||||
One LXC + one FastAPI service hosts all current and future plugins (router per plugin).
|
||||
|
||||
Source repo: gitea `dtoro/terminalito` (app code). This repo only documents the fabric
|
||||
wiring, same split as Artifacto/Plato.
|
||||
|
||||
## Current state
|
||||
|
||||
- No TRMNL middleware in the lab. Highest LXC id is 127 (see `containers/index.md`).
|
||||
- Public hostnames terminate at the [VPS netbird traefik](../hosts/netbird-vps.md) → netbird
|
||||
mesh → [caddy (121)](../containers/121-caddy.md) → backend LXC. Cert obtained by Caddy
|
||||
(IONOS DNS-01) and mirrored to the VPS by the daily cert-sync timer on the host.
|
||||
- Auto-deploy pipelines are gitea-webhook driven, two shapes (see [auto-deploy](../infrastructure/auto-deploy.md)).
|
||||
|
||||
## Target state
|
||||
|
||||
```
|
||||
TRMNL cloud --GET 15m, Bearer token--> https://trmnl.hubris.network/munich-home/dashboard
|
||||
VPS traefik (public TLS) --netbird--> caddy (121) --> trmnl (128) :9851 trmnl-plugins.service
|
||||
├ Open-Meteo (weather)
|
||||
├ MVG departures (transit)
|
||||
└ Google Calendar (OAuth, SOPS)
|
||||
```
|
||||
|
||||
- **LXC 128 `trmnl`**: Debian, unprivileged, ~1 core / 512 MiB–1 GiB / 8 GiB rootfs. No mounts.
|
||||
- **Service** `trmnl-plugins.service` → `uvicorn server.app:app --host 0.0.0.0 --port 9851`,
|
||||
`EnvironmentFile=/etc/trmnl-plugins/env`. Auth: every path except `/health` requires
|
||||
`Authorization: Bearer $TRMNL_POLL_TOKEN`.
|
||||
- **Auto-deploy** (Shape B): `/opt/terminalito` working tree, sibling `/opt/terminalito-deploy/`.
|
||||
- Public hostname `trmnl.hubris.network`.
|
||||
|
||||
## Pre-flight checklist
|
||||
|
||||
- [ ] Confirm next free LXC id is 128 (`homelab list`, `containers/index.md`).
|
||||
- [ ] Decide IP on `192.168.8.0/16` LAN (e.g. `192.168.8.211`) — pick a free one.
|
||||
- [ ] Have Google OAuth client + refresh token, MVG stop globalIds, and a generated
|
||||
`trmnl_poll_token` ready for the secret (see `dtoro/terminalito` README).
|
||||
|
||||
## Step-by-step procedure
|
||||
|
||||
1. **Provision + enroll**
|
||||
```bash
|
||||
# create LXC 128 trmnl on hubris (Debian), then enroll it:
|
||||
homelab client add trmnl # joins netbird, provisions /etc/age/key.txt, edits inventory.yaml
|
||||
homelab client add --finalize-pubkey <age_pubkey> # commits the age pubkey
|
||||
ssh trmnl 'apt-get install -y python3-venv git'
|
||||
```
|
||||
|
||||
2. **Secret** (`trmnl-oauth`): create `secrets/trmnl-oauth.yaml` with
|
||||
`google_client_id/secret/refresh_token`, MVG stop ids, and `trmnl_poll_token`; add a
|
||||
`path_regex` rule in `.sops.yaml` granting **trmnl**'s age pubkey; `sops updatekeys`.
|
||||
The service reads it at deploy time via `homelab secret trmnl-oauth` → `/etc/trmnl-plugins/env`.
|
||||
|
||||
3. **App + service** on LXC 128 (clone uses the **internal** gitea URL — `git.hubris.network`
|
||||
resets from inside LXCs):
|
||||
```bash
|
||||
git clone http://192.168.8.121:3000/dtoro/terminalito.git /opt/terminalito
|
||||
python3 -m venv /opt/terminalito/server/.venv
|
||||
/opt/terminalito/server/.venv/bin/pip install -r /opt/terminalito/server/requirements.txt
|
||||
# install /etc/systemd/system/trmnl-plugins.service, enable --now
|
||||
```
|
||||
|
||||
4. **Deploy pipeline** (Shape B, mirrors homelab-mcp): create `/opt/terminalito-deploy/`
|
||||
`{webhook.py,deploy.sh}` (HMAC vs `/etc/terminalito-deploy/secret`, filter `refs/heads/main`,
|
||||
`git pull` → `pip install -r server/requirements.txt` → rebuild env from `homelab secret` →
|
||||
`systemctl restart trmnl-plugins`). Receiver `:9797`. Git creds at
|
||||
`/etc/terminalito-deploy/git-credentials` (mode 600). Register a gitea webhook on
|
||||
`dtoro/terminalito`; add `192.168.8.<128-ip>` to gitea `app.ini` `ALLOWED_HOST_LIST`.
|
||||
|
||||
5. **DNS**: add `trmnl.hubris.network` A → `192.168.8.175` (caddy) on [Technitium (107)](../containers/107-dns.md).
|
||||
|
||||
6. **Caddy** (`dtoro/caddy-conf`, commit+push auto-deploys):
|
||||
```
|
||||
trmnl.hubris.network { reverse_proxy 192.168.8.<128-ip>:9851 }
|
||||
```
|
||||
|
||||
7. **Public exposure** on the [VPS](../hosts/netbird-vps.md): add traefik router+service for
|
||||
`Host(\`trmnl.hubris.network\`)` → `http://192.168.8.<128-ip>:9851`; add the host to the
|
||||
cert-sync map so the LE cert mirrors over.
|
||||
|
||||
8. **TRMNL cloud**: create a Polling private plugin, URL `…/munich-home/dashboard`, header
|
||||
`Authorization: Bearer <trmnl_poll_token>`, refresh 15 min; paste `full.liquid`; add to a playlist.
|
||||
|
||||
## Verification
|
||||
|
||||
- `ssh trmnl systemctl is-active trmnl-plugins` → `active`; `curl -s localhost:9851/health` → `ok`.
|
||||
- LAN: `curl -s -H "Authorization: Bearer <tok>" https://trmnl.hubris.network/munich-home/dashboard`
|
||||
→ 200 JSON; without the header → 401; `/health` → 200.
|
||||
- Public: same curl from off-mesh resolves via VPS, 200.
|
||||
- Push a no-op commit → `/opt/terminalito-deploy` logs show pull+restart; webhook 202.
|
||||
- `homelab mcp get_host trmnl` and `search_docs trmnl` agree with `containers/128-trmnl.md`.
|
||||
|
||||
## Post-migration
|
||||
|
||||
When executed, write changelog entries (same date) on:
|
||||
`containers/128-trmnl.md` (new page), `containers/index.md` (row), `inventory.yaml`
|
||||
(`services.trmnl`), `infrastructure/auto-deploy.md` (pipeline row + `ALLOWED_HOST_LIST` +
|
||||
changelog), `containers/104-gitea.md` (webhook + allowed host), `containers/121-caddy.md`
|
||||
(new proxied host), `containers/107-dns.md` (A record), `hosts/netbird-vps.md` (public route +
|
||||
cert-sync). Then set this plan's status to `Done` in `plans/index.md`.
|
||||
@@ -6,6 +6,7 @@ Pre-flight runbooks for planned changes that haven't happened yet. Once executed
|
||||
|
||||
| Date | Title | Status |
|
||||
| ---- | ----- | ------ |
|
||||
| 2026-06-24 | [TRMNL plugins LXC (128) + middleware deploy pipeline](2026-06-24-trmnl-plugins-lxc.md) | Planned |
|
||||
| 2026-06-01 | [Slate AX → SODOLA managed switch migration](2026-06-01-slate-ax-to-sodola-migration.md) | Done |
|
||||
|
||||
## Conventions
|
||||
|
||||
Reference in New Issue
Block a user