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>
5.7 KiB
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 → netbird mesh → caddy (121) → 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).
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/healthrequiresAuthorization: Bearer $TRMNL_POLL_TOKEN. - Auto-deploy (Shape B):
/opt/terminalitoworking 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/16LAN (e.g.192.168.8.211) — pick a free one. - Have Google OAuth client + refresh token, MVG stop globalIds, and a generated
trmnl_poll_tokenready for the secret (seedtoro/terminalitoREADME).
Step-by-step procedure
-
Provision + enroll
# 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' -
Secret (
trmnl-oauth): createsecrets/trmnl-oauth.yamlwithgoogle_client_id/secret/refresh_token, MVG stop ids, andtrmnl_poll_token; add apath_regexrule in.sops.yamlgranting trmnl's age pubkey;sops updatekeys. The service reads it at deploy time viahomelab secret trmnl-oauth→/etc/trmnl-plugins/env. -
App + service on LXC 128 (clone uses the internal gitea URL —
git.hubris.networkresets from inside LXCs):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 -
Deploy pipeline (Shape B, mirrors homelab-mcp): create
/opt/terminalito-deploy/{webhook.py,deploy.sh}(HMAC vs/etc/terminalito-deploy/secret, filterrefs/heads/main,git pull→pip install -r server/requirements.txt→ rebuild env fromhomelab secret→systemctl restart trmnl-plugins). Receiver:9797. Git creds at/etc/terminalito-deploy/git-credentials(mode 600). Register a gitea webhook ondtoro/terminalito; add192.168.8.<128-ip>to giteaapp.iniALLOWED_HOST_LIST. -
DNS: add
trmnl.hubris.networkA →192.168.8.175(caddy) on Technitium (107). -
Caddy (
dtoro/caddy-conf, commit+push auto-deploys):trmnl.hubris.network { reverse_proxy 192.168.8.<128-ip>:9851 } -
Public exposure on the VPS: 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. -
TRMNL cloud: create a Polling private plugin, URL
…/munich-home/dashboard, headerAuthorization: Bearer <trmnl_poll_token>, refresh 15 min; pastefull.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-deploylogs show pull+restart; webhook 202. homelab mcp get_host trmnlandsearch_docs trmnlagree withcontainers/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.