108 lines
5.8 KiB
Markdown
108 lines
5.8 KiB
Markdown
# 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.yaml) → netbird
|
||
mesh → [caddy (121)](../knowledge/wiki/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](../knowledge/wiki/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)](../knowledge/wiki/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.yaml): 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`.
|