Phase 1-4: full doc reorg

Phase 1 — fix stale state after strong migration (Phase 1+2, 2026-07-05)
  - README: corrected IPs (jellyfin 206→246, arriman 132→245, etc.),
    added missing containers (128 trmnl, 129 house, 133 seanime, 134 romm,
    124 authentik), updated last-refreshed date, added strong host context
  - containers/101-jellyfin.md: IP 206→246, host hubris→strong, mount
    /mnt/library→/mnt/media_local, GPU 760M→680M+RX7600, privilege→priv
  - containers/118-elementsynapse.md: IP 239→242, added Host: strong
  - containers/122-arriman.md: IP 132→245, mount→/mnt/media_local, added Host
  - containers/129-house.md: IP 212→244, added Host: strong
  - containers/130-grimmory.md: IP 213→247, mount→/mnt/media_local, added Host
  - containers/121-caddy.md: fixed site list (books→grimmory, removed auth→VPS,
    added house, roms, teddy, trmnl)
  - hosts/strong.md: updated At-a-glance to reflect 7 LXCs hosted
  - containers/123-claudio-bot.md, 127-mule-photos-new.md: archived to
    containers/archive/ (were destroyed LXCs with living pages)
  - inventory.yaml: verified correct — no changes needed

Phase 2 — structural cleanup
  - infrastructure/index.md: one-page overview of all cross-cutting systems
  - runbooks/: moved runbook-budget-from-csv.md and runbook-dpkg-interrupted.md
    from operations/ with YAML frontmatter added
  - plans/done/: moved 4 completed plans out of active view; updated index
  - vms/index.md: added VM index page

Phase 3 — navigation & discoverability
  - GLOSSARY.md: term definitions (Authentik, Caddy, LXC, VAAPI, etc.)
  - README: added table of contents, links to glossary + infrastructure index
  - investigations/: archived 2 resolved cases (crash-loop, authentik-migration)
    to investigations/archive/; updated index with active vs archived sections

Phase 4 — ongoing discipline
  - CONTRIBUTING.md: documented same-session update rule with explicit checklist
  - README: replaced full LXC table with summary + link to containers/index.md
    (single source of truth; de-duplication)
This commit is contained in:
2026-07-06 00:45:28 +02:00
parent 205d8a1a43
commit fd35b48c8d
26 changed files with 255 additions and 58 deletions

View File

@@ -0,0 +1,131 @@
# 2026-06-01 — Slate AX → SODOLA Migration
**Status:** Done — 2026-06-02
**Hardware:** SODOLA 5-Port 2.5Gbit Managed Switch replacing GL.iNet Slate AX
**Router:** Fritz!Box 7590
## Goal
Remove the Slate AX sub-router. It adds double-NAT, double port-forward complexity, and a second DHCP/gateway hop. Replace with the SODOLA switch so Proxmox and homelab devices attach directly to the Fritz!Box—without changing any of their IPs.
## Current topology
```
ISP
└── Fritz!Box 7590 (192.168.178.1)
└── GL.iNet Slate AX WAN: 192.168.178.x LAN: 192.168.8.1 [NAT]
├── hubris (Proxmox) 192.168.8.77
├── + all 16 LXCs on vmbr0 bridged to the same subnet
└── other homelab devices
```
## Target topology
```
ISP
└── Fritz!Box 7590
├── VLAN 1 192.168.178.0/24 (main LAN — laptops, phones)
└── VLAN 10 192.168.8.0/24 gateway 192.168.8.1 (homelab, no NAT)
└── SODOLA 5-Port 2.5Gbit
├── Port 1 uplink → Fritz!Box LAN port
├── Port 2 hubris (Proxmox) 192.168.8.77
├── Port 3 [homelab device]
├── Port 4 [homelab device]
└── Port 5 spare
```
Fritz!Box takes over `192.168.8.1` — the same gateway IP the Slate AX used. No static IPs or gateway entries change on any LXC or VM.
See [network architecture](../infrastructure/network.md) for the permanent topology reference.
## Pre-flight checklist
- [ ] Note Slate AX gateway IP on the homelab subnet (expected `192.168.8.1`)
- [ ] Note Slate AX upstream DNS setting (what it forwards to)
- [ ] List all port forwards currently on the Slate AX → move these to Fritz!Box
- [ ] List all port forwards on Fritz!Box that point at the Slate AX WAN IP → collapse these into direct homelab-IP rules
- [ ] Confirm Fritz!OS version supports "second IP network" (Fritz!OS 7.x required)
- [ ] SSH into hubris: verify `grep gateway /etc/network/interfaces``192.168.8.1`
- [ ] SODOLA web UI accessible and ports configurable before physical move
## Fritz!Box configuration
**Path:** Home Network → Network → Network Settings → IPv4 Addresses → Add IP network
| Field | Value |
|---|---|
| IP address | `192.168.8.1` |
| Subnet mask | `255.255.255.0` |
| DHCP range | `192.168.8.100 192.168.8.240` |
| Assign to | LAN port that connects to SODOLA |
| Network isolation | Enabled (blocks main LAN from initiating into homelab) |
| DNS for DHCP clients | `192.168.8.2` (Technitium on [CT 107](../containers/107-dns.md)) |
After creating the network, move any port forwards from the Slate AX into Fritz!Box → Internet → Permits (target IPs are now directly reachable on `192.168.8.x`).
## SODOLA configuration
All ports in VLAN 10, untagged (simple access-port setup):
| Port | PVID | Role |
|---|---|---|
| 1 | 10 | Uplink → Fritz!Box |
| 2 | 10 | hubris (Proxmox) |
| 3 | 10 | homelab device |
| 4 | 10 | homelab device |
| 5 | 10 | spare |
## Proxmox / LXC changes
If the Slate AX gateway was `192.168.8.1`: **no changes needed**. Fritz!Box takes the same IP.
If Slate AX used a different gateway (e.g. `.254`), update:
```bash
# on hubris
nano /etc/network/interfaces # change gateway line
ifreload -a
# for each LXC with a static gateway
pct config <id> # check net0: line for gw=
pct set <id> --net0 name=eth0,bridge=vmbr0,ip=<ip>/24,gw=192.168.8.1
```
## DNS after migration
Technitium ([CT 107](../containers/107-dns.md)) at `192.168.8.2` continues to serve split-horizon DNS for `hubris.network`. The Fritz!Box DHCP server for VLAN 10 hands out `192.168.8.2` as the DNS server. This fixes the "update router DHCP DNS from dead .180 → .2" outstanding item in [dns.md](../infrastructure/dns.md).
## Cutover procedure
1. Configure Fritz!Box second IP network (VLAN 10 / `192.168.8.1`) — verify DHCP works via a test laptop on the target port.
2. Configure SODOLA port VLAN assignments.
3. Connect SODOLA port 1 → Fritz!Box target LAN port.
4. Move hubris ethernet → SODOLA port 2. Proxmox should stay reachable immediately (same gateway IP).
5. Move remaining homelab devices to SODOLA ports 35.
6. Decommission Slate AX.
## Verification
```bash
# internet from Proxmox
ssh proxmox "ping -c3 1.1.1.1"
# Fritz!Box main LAN reachable (routing between VLANs works)
ssh proxmox "ping -c3 192.168.178.1"
# split-horizon DNS via Technitium still resolves
ssh proxmox "dig @192.168.8.2 +short git.hubris.network"
# expected: 192.168.8.175
# Caddy reverse proxy responds
curl -sk https://git.hubris.network | head -5
# Authentik on VPS still reachable
curl -sk https://auth.hubris.network/if/flow/default-authentication-flow/ | head -5
```
## Post-migration
- Update [network.md](../infrastructure/network.md) topology to reflect new state.
- Add changelog entries to [hosts/hubris.md](../hosts/hubris.md) and any affected container pages.
- Update status in [plans/index.md](index.md) to `Done`.
- If anything went sideways, open an investigation in `investigations/`.

View File

@@ -0,0 +1,318 @@
# Deprecate claudio-bot (LXC 123) — Hermes Agent now serves as control plane
## Goal
Phase out the **claudio-bot ecosystem** (LXC 123, claudio-monitor, IPC server)
now that Hermes Agent is configured and running on mac-mini. Hermes replaces
claudio-bot's role as the homelab control plane — accepting commands, relaying
alerts, and answering queries about infrastructure state.
## What is claudio-bot
A three-component ecosystem deployed 2026-04-20:
| Component | Location | Purpose |
|-----------|----------|---------|
| **claudio-bot** | LXC 123 (192.168.8.230) | Matrix bot `@claudio:avispero` — slash commands, NL queries, infra notifications |
| **claudio-monitor** | hubris (PVE host) | 5-min watchdog → sends alerts to claudio-bot's IPC server |
| **IPC server** | LXC 123:9090 | `/notify`, `/propose`, `/status` endpoints — used by monitor + backup wrapper |
Dependencies:
- Matrix synapse on LXC 118 (elementsynapse) — claudio-bot connects to `192.168.8.239:8008`
- Gitea auto-deploy webhook → LXC 123:9797 (pipeline `dtoro/claudio-bot`)
- LLM backend: LM Studio on mac-mini (192.168.8.174:1234) or Anthropic API
- Secrets at `/etc/claudio-bot/` (config.yaml, matrix.token, anthropic.key, lmstudio.key, ipc.token)
## What Hermes replaces
| claudio-bot function | Hermes replacement |
|---------------------|-------------------|
| Matrix slash commands | Hermes CLI + `send_message` for outbound notifications |
| NL infrastructure queries | Hermes with `homelab` CLI + MCP tools (already working) |
| Monitor alert relay | Hermes-native: (a) on-demand `homelab health` via extended skill, (b) cronjob every 15 min with actionable alerts (not just passive notifications) |
| IPC server (port 9090) | **Retired** — Hermes doesn't need a separate IPC layer; monitoring runs directly via SSH + `send_message` |
| LLM backend | Already handled by Hermes (OpenRouter → deepseek/deepseek-v4-pro) |
| Auto-deploy webhook (9797) | **Retired** — repo archived |
## Full blast radius (files referencing claudio)
### Homelab-context repo (`/opt/homelab-context/`)
#### Core topology
- `inventory.yaml:275``claudio-bot:` host entry (LXC 123, matrix-agent)
- `hosts/claudio-bot.yaml` — auto-generated host file (delete)
- `.sops.yaml:47` — placeholder comment listing claudio-bot/claudio-monitor
#### Documentation (containers)
- `containers/123-claudio-bot.md` — full container doc
- `containers/index.md:17` — LXC table row
- `containers/118-elementsynapse.md:3,36,39``@claudio:avispero` reference, disk reading, direct connection note
- `containers/103-paperless.md:27` — disk reading from claudio-monitor
#### Documentation (infrastructure)
- `infrastructure/monitoring.md`**ENTIRE FILE** is about claudio-monitor → claudio-bot pipeline
- `infrastructure/backups.md:74,94,98` — IPC notify, thermal monitoring moved out, related link
- `infrastructure/auto-deploy.md:23,41,50,61,120,138-139` — Gitea webhook target, pipeline table, port 9797
- `infrastructure/ssh-access.md:134` — LXC table row
- `infrastructure/homelab-context.md:4,16,143` — client list, integration note
#### Other
- `operations/commands.md:17``pvesh` bulk query note (passive reference, can stay)
- `README.md:32,40,64` — LXC table, monitoring link, tracked configs mention
- `mcp/deploy/webhook/webhook.py:5-6` — port comment (9797, 9799)
- `hosts/hubris.md:57,96``/opt/claudio-monitor` checkout, LXC 109 cleanup note (passive)
- `vms/108-haos.md:30` — claudio-monitor MQTT removal note (historical, can stay)
- `investigations/2026-04-21-hubris-crash-loop.md:27` — historical reference (keep)
### External repos (Gitea)
- `dtoro/claudio-bot` — the bot itself
- `dtoro/claudio-monitor` — the watchdog scripts
### External state (hubris)
- `/opt/claudio-bot/` — bot checkout (LXC 123)
- `/opt/claudio-monitor/` — monitor checkout (hubris host)
- `/etc/claudio-bot/` — secrets directory
- `/etc/claudio-deploy/` — deploy git credentials
- `/etc/claudio-monitor/` — thresholds + bot.token
- `/var/lib/claudio-monitor/` — state files (.alert, .counter)
- `claudio-bot.service` — systemd unit (LXC 123)
- `claudio-monitor.timer` + `.service` — systemd timer (hubris)
- Gitea webhook id 6 (claudio-bot-deploy) + webhook for claudio-monitor (9799) on apps LXC 105
### External state (workstations)
- Nothing on mac-mini, republic-laptop, or ludo-mini references claudio directly
## Phased deprecation plan
### Phase 1 — Stop & verify (low-risk, reversible)
**Goal**: Stop claudio services, verify nothing breaks, keep LXC 123 paused (not destroyed).
1. **Stop claudio-monitor on hubris**
```bash
ssh hubris systemctl disable --now claudio-monitor.timer
```
The 5-minute watchdog stops. Home Assistant still gets PVE metrics independently via its own integration — no alerting gap there.
2. **Stop claudio-bot on LXC 123**
```bash
ssh claudio-bot systemctl stop claudio-bot
```
The Matrix bot goes offline. Nobody can send slash commands to `@claudio:avispero`.
3. **Pause LXC 123** (not destroy yet)
```bash
ssh hubris pct shutdown 123
```
IPC server (port 9090) and deploy webhook (port 9797) become unreachable.
4. **Verify no breakage** (run for 24-48h)
- Check Gitea webhook logs — the `dtoro/claudio-bot` webhook will 404 on pushes (non-fatal)
- Check Caddy logs — no upstream errors for claudio-related paths (there shouldn't be any — no public hostname)
- Check DNS — no queries for claudio-bot.hubris.network (doesn't have a public hostname)
- Confirm Hermes works normally for all homelab operations
### Phase 2 — Hermes-native health monitoring (replaces claudio-monitor)
**Goal**: Two capabilities that together exceed what claudio-monitor did:
1. **On-demand health check** — user asks "how's the homelab?" → Hermes runs a
comprehensive check across all hosts/LXCs and reports results conversationally.
2. **Cron-based watchdog** — runs every 15 minutes, checks thresholds, raises
alarms with actionable options (not just passive alerts). User can respond
and Hermes takes action directly.
**Key difference from claudio-monitor**: The old system was a bash script that
POSTed JSON to an IPC server that a Matrix bot reformatted into a flat message.
The Hermes-native approach gives the LLM the raw metrics and lets it reason about
what's important before alerting — and crucially, the user can reply to an alert
and Hermes can SSH into hubris to resize a rootfs, restart a service, or
investigate deeper.
#### 2a. Extend `homelab-hardware-health` skill
The existing skill covers CPU/NVMe temps. Expand it to be the **single
homelab health check skill** covering:
- **Hardware**: CPU temps (sysfs), NVMe/SATA SSD temps (smartctl), block device inventory
- **LXC resources**: `pvesh get /cluster/resources --type vm --output-format json` → disk %, CPU %, RAM usage per container
- **Service health**: Check key services respond (gitea:3000, caddy:80/443, synapse:8008, paperless, nextcloud, jellyfin)
- **System drift**: `apt list --upgradable` counts per host, docker image pull digests on apps LXC 105
Thresholds (from old `/etc/claudio-monitor/thresholds.env`):
| Metric | Warning | Critical |
|--------|---------|----------|
| LXC disk usage | > 80% | > 90% |
| NVMe temp | > 60°C | > 70°C |
| CPU temp | > 70°C | > 80°C |
| Apt upgradable | > 10 | > 50 |
| Service down | any | 2+ services |
Update the skill file at `devops/homelab-hardware-health/SKILL.md` with:
- Expanded trigger phrases ("check homelab health", "how's the lab", "health report")
- LXC resource check commands
- Service health check commands
- Thresholds table
- "If you find issues, present options to the user" instruction
#### 2b. Create Hermes cronjob `homelab-health-watchdog`
```bash
hermes cron create \
--schedule 'every 15m' \
--name 'homelab-health-watchdog' \
--prompt 'Load skill homelab-hardware-health. Run a full health check across all hosts (hubris + all LXCs). Check: (1) hardware temps, (2) LXC disk/CPU/RAM via pvesh, (3) service reachability for gitea, caddy, synapse, paperless, nextcloud, jellyfin, (4) apt upgradable counts. Compare against thresholds in the skill. If nothing is critical, say nothing (no message = healthy). If anything breaches thresholds, send a concise alert with: what broke, current value vs threshold, and 2-3 actionable options the user can reply to. DO NOT send routine "all clear" messages.'
```
This runs every 15 minutes (Hermes cron is LLM-driven, heavier than a bash
script — 15 min is appropriate vs claudio-monitor's 5 min). When thresholds
are clean, the agent stays silent. When something breaches, the user gets a
message like:
> ⚠️ LXC 103 (paperless) disk at 91% (> 90% critical)
>
> Options: [resize rootfs to 16 GiB] [investigate what's using space] [snooze 24h]
User picks an option → Hermes takes action (SSH into hubris, run `pct resize`,
grow the filesystem inside the container).
#### 2c. Add `homelab-health` alias to `homelab` CLI
For quick on-demand checks from the terminal:
```bash
homelab health # runs the skill inline via Hermes
# or equivalently:
hermes --prompt "Load homelab-hardware-health, run full health check"
```
### Phase 3 — Documentation & inventory cleanup
**Goal**: Update homelab-context to reflect claudio deprecation, without destroying history.
#### Files to edit
| File | Action |
|------|--------|
| `inventory.yaml` | Mark `claudio-bot` entry as deprecated (comment + `status: deprecated`). Do NOT remove — keeps historical ID mapping. |
| `containers/123-claudio-bot.md` | Add deprecation banner at top, changelog entry |
| `containers/index.md` | Change status from "running" to "stopped (deprecated)" |
| `infrastructure/monitoring.md` | Rewrite to document new Hermes-based monitoring. Keep claudio-monitor section as "legacy (deprecated 2026-06-04)". |
| `infrastructure/auto-deploy.md` | Mark claudio-bot and claudio-monitor webhook entries as deprecated |
| `infrastructure/backups.md` | Update notification section (IPC path no longer available). Note: backups themselves are still disabled — this is forward-looking. |
| `infrastructure/ssh-access.md` | Mark LXC 123 as "offline (deprecated)" |
| `infrastructure/homelab-context.md` | Remove claudio-bot from client list |
| `containers/118-elementsynapse.md` | Note `@claudio:avispero` is decommissioned |
| `containers/103-paperless.md` | Remove stale disk reading reference (already resolved — rootfs grew to 16 GiB 2026-05-15 per 118 doc) |
| `README.md` | Update LXC table, remove monitoring link |
| `hosts/claudio-bot.yaml` | Add deprecation banner (or delete if auto-generated — it is, so just update inventory) |
| `.sops.yaml` | Remove claudio-bot from placeholder comment |
#### Files to leave untouched (historical)
| File | Reason |
|------|--------|
| `investigations/2026-04-21-hubris-crash-loop.md` | Historical investigation — claudio-monitor is factual context |
| `vms/108-haos.md:30` | Historical note about MQTT removal — factual |
| `mcp/deploy/webhook/webhook.py:5-6` | Comment only — harmless, keep for port mapping context |
| `operations/commands.md:17` | Passive reference — harmless |
### Phase 4 — Purge (after confirmation window)
**Goal**: Full removal after confirming Phase 1-3 is stable for ≥2 weeks.
1. **Destroy LXC 123**
```bash
ssh hubris pct destroy 123 --purge
```
Frees 8 GiB rootfs, 512 MiB RAM, 1 core.
2. **Remove claudio-monitor from hubris**
```bash
ssh hubris rm -rf /opt/claudio-monitor /etc/claudio-monitor /var/lib/claudio-monitor
ssh hubris systemctl disable claudio-monitor.timer # already done in phase 1
```
3. **Archive Gitea repos**
- Archive `dtoro/claudio-bot` (mark read-only in Gitea)
- Archive `dtoro/claudio-monitor` (mark read-only in Gitea)
4. **Remove Gitea webhooks**
- Remove claudio-bot-deploy webhook (id 6) from Gitea
- Remove claudio-monitor webhook (9799) from apps LXC 105
- Remove `192.168.8.230` from Gitea `app.ini` `ALLOWED_HOST_LIST`
5. **Final documentation cleanup**
- Remove `containers/123-claudio-bot.md` (or move to `containers/_destroyed/`)
- Remove `hosts/claudio-bot.yaml`
- Remove claudio-bot row from `containers/index.md`
- Update all cross-references (see blast radius above)
## Files changed by phase
### Phase 3 (documentation)
- `/opt/homelab-context/inventory.yaml`
- `/opt/homelab-context/containers/123-claudio-bot.md`
- `/opt/homelab-context/containers/index.md`
- `/opt/homelab-context/infrastructure/monitoring.md`
- `/opt/homelab-context/infrastructure/auto-deploy.md`
- `/opt/homelab-context/infrastructure/backups.md`
- `/opt/homelab-context/infrastructure/ssh-access.md`
- `/opt/homelab-context/infrastructure/homelab-context.md`
- `/opt/homelab-context/containers/118-elementsynapse.md`
- `/opt/homelab-context/containers/103-paperless.md`
- `/opt/homelab-context/README.md`
- `/opt/homelab-context/hosts/claudio-bot.yaml`
- `/opt/homelab-context/.sops.yaml`
### Phase 4 (destruction)
- Proxmox: LXC 123 destroyed
- Gitea: `dtoro/claudio-bot` archived, `dtoro/claudio-monitor` archived
- Gitea webhooks removed (apps LXC 105)
- hubris: `/opt/claudio-monitor/`, `/etc/claudio-monitor/`, `/var/lib/claudio-monitor/` removed
- `containers/123-claudio-bot.md` → moved to `_destroyed/` or deleted
## Risks & tradeoffs
| Risk | Mitigation |
|------|-----------|
| **Monitoring gap** — no alerts during Phase 1-2 transition | Phase 1 stops the old timer but Phase 2 builds the Hermes-native replacement immediately. If there's a gap, keep claudio-monitor.timer running until the Hermes cronjob is verified with a test alert. |
| **Backup notifications** — the backup wrapper's IPC notify path is dead | Backups are already DISABLED. When re-enabled, wire them to the new Hermes monitoring pipeline instead. |
| **Matrix `@claudio:avispero`** — users may still try to interact | The bot just won't respond. Could leave a notice in the Matrix room. |
| **Gitea webhook failures** — pushes to `dtoro/claudio-bot` will 404 | Non-fatal. Archive the repo in Phase 4 to stop pushes entirely. |
| **Losing monitoring history** — state files on hubris | Keep `/var/lib/claudio-monitor/` until Phase 4. No rush. |
| **LM Studio dependency** — claudio-bot was using mac-mini's LM Studio | Hermes already uses OpenRouter. LM Studio on mac-mini can stay for other uses or be reclaimed. |
## Open questions
1. **Should the Matrix `@claudio:avispero` account be deactivated or left as-is?** It has no public hostname and was invite-only to a private room.
2. **Are you OK archiving `dtoro/claudio-bot` and `dtoro/claudio-monitor` on Gitea (read-only, preserved), or do you want them fully deleted?**
3. **Should LXC 123 be paused (reversible) or destroyed immediately?** Recommend pause for at least 2 weeks as a safety net.
4. **Which messaging platform should the health watchdog deliver alerts to?** Currently Hermes is connected via CLI (mac-mini). To get push alerts, we need a `send_message` target — Telegram, Discord, or Matrix are available. Which do you prefer?
## Verification
After Phase 1:
- `ssh hubris systemctl is-active claudio-monitor.timer` → `inactive`
- `ssh claudio-bot systemctl is-active claudio-bot` → `inactive` (or unreachable if LXC stopped)
- `ssh hubris pct status 123` → `stopped`
- `curl -s http://192.168.8.230:9090/status` → connection refused (IPC dead)
After Phase 2:
- `hermes cron list` shows `homelab-health-watchdog` running every 15 min
- "check homelab health" → Hermes runs comprehensive check, reports temps + LXC resources + service status + apt drift
- Force an alert: `NVME_WARN_TEMP_C=1` on hubris → watchdog catches it on next 15-min tick → alert arrives via Hermes messaging with actionable options
- Alert format includes 2-3 options user can reply to, not just passive notification
After Phase 3:
- All cross-references in homelab-context point to deprecated/removed status
- `grep -r 'claudio' /opt/homelab-context/ --include='*.md' --include='*.yaml' | grep -v '_destroyed\|investigations\|changelog'` → only historical references remain
After Phase 4:
- `ssh hubris pct list | grep 123` → no output
- `ssh hubris ls /opt/claudio-monitor` → no such file
- Gitea `dtoro/claudio-bot` → archived badge

View File

@@ -0,0 +1,443 @@
# Yuvomi deployment — `house.hubris.network`
Deploy [Yuvomi](https://yuvomi.cloud/) (previously Oikos) — a self-hosted
family planner with 14 modules (calendar, tasks, meals, groceries, budget,
documents, notes, etc). Single Docker container (Express.js + SQLCipher
SQLite), 256 MB RAM min.
**Target hostname:** `house.hubris.network` — publicly reachable via VPS
traefik, LAN reachable via Caddy.
**Integrations:**
- Authentik SSO (OIDC)
- Google Calendar (tokens exist on trmnl LXC 128)
- Paperless (Yuvomi's Documents module / clarification needed — see Phase 4)
---
## Phase 0 — Clarifications needed
### 0.1 Paperless connection
Yuvomi's "Documents" module stores documents inside its encrypted SQLite DB or
optionally on WebDAV. There is **no direct Paperless-ngx API connector** in
Yuvomi. Options:
a) **Keep as-is** — Yuvomi's docs are separate from Paperless, no integration
b) **WebDAV bridge** — Mount Paperless's consumption dir as WebDAV, point
Yuvomi doc storage there (Yuvomi stores newly uploaded docs directly in the
Paperless consume folder)
c) **Custom module** — Write a Yuvomi module that fetches from Paperless API
Decision needed before Phase 3 config.
**Decision:** WebDAV bridge (Phase 6.2).
### 0.2 Deployment target
Two options:
| Option | Pros | Cons |
|--------|------|------|
| **apps LXC (105)** — Docker already there, 4GB RAM, 2 cores | Zero provisioning, existing compose pattern | Shared with artifacto, MCP, secrets-issuance; Portainer-managed stacks can be tricky |
| **New LXC (~129)** — dedicated, clean | Isolated, no side-effects | Need to create, install Docker, wire into everything |
**Decision:** New LXC (129).
---
## Phase 1 — Provision new LXC (129) for Yuvomi
### 1.1 Create the LXC on hubris
```
ssh root@192.168.8.77 << 'EOF'
# Check available templates
pveam list local | grep debian
# Create unprivileged Debian 13 LXC (follows trmnl's unpriv pattern)
pct create 129 local:vztmpl/debian-13-standard_13.7-1_amd64.tar.zst \
--hostname house \
--description "Yuvomi family planner — house.hubris.network" \
--cores 1 \
--memory 1024 \
--swap 512 \
--rootfs local:8 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp,type=veth \
--unprivileged 1 \
--features nesting=1 \
--onboot 1 \
--start 1
EOF
```
Resources: 1 core / 1 GiB RAM / 8 GiB rootfs (generous for a single Express.js
container; can downsize later).
### 1.2 Set static IP and install Docker
After the LXC boots, find its DHCP lease, then set a static IP:
```
# Find actual IP
ssh root@192.168.8.77 'lxc-attach 129 -- ip addr show eth0 | grep inet'
# Reserve 192.168.8.212 (or whatever is free) via Technitium DHCP,
# or set static IP in PVE config:
ssh root@192.168.8.77 'pct set 129 --net0 name=eth0,bridge=vmbr0,ip=192.168.8.212/24,gw=192.168.8.1,type=veth'
ssh root@192.168.8.77 'lxc-attach 129 -- reboot'
```
### 1.3 Install Docker inside the LXC
```
ssh root@192.168.8.77 << 'DOCKER'
lxc-attach 129 -- bash -c '
apt-get update
apt-get install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
systemctl enable --now docker
docker --version
docker compose version
'
DOCKER
```
### 1.4 Download Yuvomi and start
```
ssh root@192.168.8.77 'lxc-attach 129 -- bash -c "
mkdir -p /opt/yuvomi /opt/yuvomi/data /opt/yuvomi/backups /opt/yuvomi/modules
cd /opt/yuvomi
curl -O https://raw.githubusercontent.com/ulsklyc/yuvomi/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/ulsklyc/yuvomi/main/.env.example
cp .env.example .env
"'
```
### 1.5 Generate keys and configure .env
```
ssh root@192.168.8.77 'lxc-attach 129 -- bash -c "
SESSION_SECRET=\$(openssl rand -hex 32)
DB_KEY=\$(openssl rand -hex 32)
cd /opt/yuvomi
sed -i \"s/SESSION_SECRET=.*/SESSION_SECRET=\$SESSION_SECRET/\" .env
sed -i \"s/DB_ENCRYPTION_KEY=.*/DB_ENCRYPTION_KEY=\$DB_KEY/\" .env
sed -i \"s/OIKOS_HTTP_PORT=3000/OIKOS_HTTP_PORT=3000/\" .env
sed -i \"s/# TZ=.*/TZ=Europe\\/Berlin/\" .env
echo \"SESSION_SECURE=true\" >> .env
echo \"TRUST_PROXY=1\" >> .env
echo \"BASE_URL=https://house.hubris.network\" >> .env
"'
```
### 1.6 Start Yuvomi
```
ssh root@192.168.8.77 'lxc-attach 129 -- bash -c "cd /opt/yuvomi && docker compose up -d"'
```
### 1.7 Verify
```
ssh root@192.168.8.77 'lxc-attach 129 -- curl -s http://127.0.0.1:3000/health'
# Expected: 200 OK
---
## Phase 2
### 2.1 Caddy — add `house.hubris.network`
Edit `/etc/caddy/Caddyfile` on LXC 121 (via `dtoro/caddy-conf` repo):
```
house.hubris.network {
tls {
dns ionos
}
reverse_proxy 192.168.8.212:3000
}
```
- Commit to `dtoro/caddy-conf` → auto-deploy via webhook
- If not yet deployed, push manually: `cd /etc/caddy && git add Caddyfile && git commit -m 'add house.hubris.network → yuvomi' && git push`
### 2.2 Verify LAN access
```
curl -sI https://house.hubris.network/
# Expected: 200 or 302 (redirect to /login or the setup wizard)
```
### 2.3 DNS — add Technitium record
Add A record `house.hubris.network → 192.168.8.175` (Caddy) on DNS LXC (107).
If using the DNS web UI: http://192.168.8.2/ → Zones → hubris.network → Add A record.
### 2.4 DNS mesh sync
If mesh DNS (Netbird managed zone) is in use, add the same record there or
verify dns-sync picks it up.
---
## Phase 3 — Public exposure (VPS traefik)
### 3.1 Add cert sync entry
On hubris (PVE host), edit `/usr/local/bin/hubris-public-cert-sync.sh`, add:
```bash
[house.hubris.network]="house.fullchain.crt house.privkey.key"
```
Run once:
```
systemctl start hubris-public-cert-sync.service
```
Verify certs landed on VPS:
```
ssh root@100.122.165.149 "ls -la /var/lib/docker/volumes/opt_netbird_traefik_letsencrypt/_data/house.*"
```
### 3.2 Add traefik router
On the VPS, edit `/opt/traefik-dynamic.yaml`:
```yaml
http:
routers:
house-public:
rule: 'Host(`house.hubris.network`)'
entryPoints:
- websecure
priority: 10
tls: {}
middlewares:
- house-ratelimit
service: house-public
middlewares:
house-ratelimit:
rateLimit:
average: 30
period: 1s
burst: 60
services:
house-public:
loadBalancer:
servers:
- url: 'http://192.168.8.212:3000'
tls:
certificates:
- certFile: /letsencrypt/house.fullchain.crt
keyFile: /letsencrypt/house.privkey.key
```
Restart traefik:
```
docker restart netbird-traefik
```
### 3.3 Verify public access
From outside the homelab LAN (or with `--resolve`):
```
curl -sI --resolve house.hubris.network:443:82.165.190.79 https://house.hubris.network/
# Expected: 200 or 302
echo | openssl s_client -connect 82.165.190.79:443 -servername house.hubris.network 2>&1 | openssl x509 -noout -subject
# Expected: CN=house.hubris.network (not TRAEFIK DEFAULT CERT)
```
---
## Phase 4 — Authentik SSO (OIDC)
### 4.1 Create OIDC provider in Authentik
Via VPS admin UI (`https://auth.hubris.network/if/admin/`):
- Applications → Providers → Create → OAuth2/OpenID Provider
- Name: `yuvomi`
- Client ID: auto-generated
- Client Secret: auto-generated (save this)
- Redirect URIs: `https://house.hubris.network/oauth2/callback`
- Signing Key: auto-generated
- Subject Mode: Based on User ID (or Based on Username — pick what Yuvomi expects)
### 4.2 Create application in Authentik
- Applications → Applications → Create
- Name: `Yuvomi`
- Slug: `yuvomi`
- Provider: select the one created above
- Launch URL: `https://house.hubris.network`
### 4.3 Set env vars in Yuvomi `.env`
On apps LXC (105), edit `/opt/yuvomi/.env`:
```
OIDC_ISSUER=https://auth.hubris.network/application/o/yuvomi/
OIDC_CLIENT_ID=<from Authentik>
OIDC_CLIENT_SECRET=<from Authentik>
# OIDC_TRUST_EMAIL_WITHOUT_VERIFIED_CLAIM=true # if Authentik doesn't send email_verified
```
### 4.4 Restart Yuvomi
```
pct exec 105 -- bash -c 'cd /opt/yuvomi && docker compose restart'
```
### 4.5 Verify SSO flow
Open `https://house.hubris.network/` — should redirect to Authentik login,
then back to Yuvomi.
---
## Phase 5 — Google Calendar
### 5.1 Extract tokens from trmnl LXC
On trmnl (LXC 128), the env file at `/etc/trmnl-plugins/env` contains:
```
GOOGLE_CLIENT_ID=119823214387-32f20ed3imesiv7uh5si7p3rou9fros4.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_REFRESH_TOKEN=1//03CS0rkuf7XVQCgYIARAAGAMSNwF-L9Irr5_b4kLhkx-dtf9EGQ1eJ1OnvxkaV_P1_4TDPwUN2lFb7nsbrZklN7qbjpkHpQyYlBY
```
### 5.2 Add Google Account redirect URI
In the Google Cloud Console (OAuth 2.0 Client IDs), add:
```
https://house.hubris.network/auth/google/callback
```
to the authorized redirect URIs for the existing client ID.
### 5.3 Set env vars in Yuvomi `.env`
```
GOOGLE_CLIENT_ID=119823214387-32f20ed3imesiv7uh5si7p3rou9fros4.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
```
Note: Yuvomi's Google Calendar integration uses the OAuth flow to get its own
refresh token — it doesn't reuse the trmnl refresh token. The first-time setup
in Yuvomi Settings → Calendar → Google Calendar will prompt for authorization.
### 5.4 Restart and verify
```
pct exec 105 -- bash -c 'cd /opt/yuvomi && docker compose restart'
```
Then in Yuvomi UI: Settings → Calendar → Connect Google Calendar → authorize.
---
## Phase 6 — Paperless integration (decide approach first)
### 6.1 If using as standalone documents module (no Paperless bridge)
No action needed. Yuvomi's Documents module works out of the box — docs stored
in encrypted SQLite.
### 6.2 If using WebDAV bridge to Paperless consumption
- Paperless consumes documents from `/mnt/library/documents/consume/`
- Point Yuvomi's WebDAV document storage at a WebDAV server serving that dir
- Options: run a lightweight WebDAV container on paperless LXC (103), or use
Nextcloud's WebDAV if documents are already in `/mnt/library`
Set env vars:
```
DOCUMENT_STORAGE_WEBDAV_ENABLED=true
DOCUMENT_STORAGE_WEBDAV_URL=http://192.168.8.130:8000/... # or WebDAV server
DOCUMENT_STORAGE_WEBDAV_USERNAME=...
DOCUMENT_STORAGE_WEBDAV_PASSWORD=...
DOCUMENT_STORAGE_WEBDAV_ALLOW_PRIVATE_NETWORK=true
```
### 6.3 If building a custom module
Write a Yuvomi module (client-side JS + module.json) that reads from
Paperless API at `https://paperless.hubris.network/api/` using a Paperless
API token. See `modules/MODULES.md` in the Yuvomi repo for the module format.
---
## Phase 7 — Backup & maintenance
### 7.1 Data persistence
Yuvomi stores everything in a single SQLCipher-encrypted SQLite file at
`/opt/yuvomi/data/oikos.db`. This is the only file needed for backup.
### 7.2 Add to homelab context
- Create `/opt/homelab-context/containers/129-yuvomi.md` (or `.../house.md`)
- Update `inventory.yaml` if using a new LXC
- Add changelog entries to caddy (121) and ingress docs
- Update `plans/index.md` → mark this plan `Done`
### 7.3 Schedule backup
Add a cron (or existing backup system) for `/opt/yuvomi/data/` if not already
covered by the host-level backup scheme.
---
## Summary of steps
| Phase | What | Who/Where |
|-------|------|-----------|
| 0 | Clarify Paperless approach + deployment target | dtoro |
| 1 | Docker Compose on apps LXC, start container | Hermes |
| 2 | Caddy block + DNS record for `house.hubris.network` | Hermes |
| 3 | VPS traefik router + cert sync for public exposure | Hermes |
| 4 | Authentik OIDC provider + env vars | Hermes (needs admin UI) |
| 5 | Google Calendar tokens + redirect URI | Hermes + dtoro (Google Cloud Console) |
| 6 | Paperless integration (depends on Phase 0 decision) | Hermes |
| 7 | Documentation, backup, inventory updates | Hermes |
---
## Duration estimate
| Phase | Time | Notes |
|-------|------|-------|
| Phase 1 | ~15 min | Download, config, startup |
| Phase 2 | ~10 min | Caddy + DNS |
| Phase 3 | ~15 min | VPS traefik + cert sync |
| Phase 4 | ~20 min | Authentik provider setup + env |
| Phase 5 | ~10 min + Google UI | Redirect URI takes 1 min in console |
| Phase 6 | TBD | Depends on chosen approach |
| Phase 7 | ~10 min | Doc + inventory updates |
| **Total** | **~1.5h + Phase 6** | |
## Rollback
If anything goes wrong:
```bash
# Stop and remove container
pct exec 105 -- bash -c 'cd /opt/yuvomi && docker compose down'
# Remove Caddy block, commit, push — auto-deploys
# Remove VPS traefik router, restart netbird-traefik
# Remove cert sync entry
# Remove DNS record
```

View File

@@ -0,0 +1,246 @@
# Plan: Migrate Booklore → Grimmory (LXC 130)
**Status:** in-progress
**Date:** 2026-06-29
**Goal:** Replace Booklore on shared apps LXC 105 with Grimmory on a dedicated LXC 130. Grimmory is the community fork/successor of Booklore with the same database schema and port, so the migration is a near-drop-in swap.
---
## Pre-flight checklist
- [ ] Note Booklore MariaDB credentials from Portainer compose on LXC 105 (`DATABASE_PASSWORD`, `MYSQL_ROOT_PASSWORD`)
- [ ] Confirm `/mnt/library/books` is readable on LXC 105 (`ls /mnt/library/books | head`)
---
## Step 1 — Dump Booklore MariaDB
```bash
# On hubris — find the MariaDB container name
pct exec 105 -- docker ps --format '{{.Names}}' | grep -i maria
# Dump (replace <CONTAINER> and <PASSWORD> from Portainer compose)
pct exec 105 -- docker exec <CONTAINER> \
mysqldump -u grimmory -p<PASSWORD> grimmory \
> /tmp/booklore-$(date +%Y%m%d).sql
# Pull to hubris root for safekeeping
pct pull 105 /tmp/booklore-$(date +%Y%m%d).sql /root/
```
---
## Step 2 — Create LXC 130
```bash
# On hubris — list available Debian 13 templates
pveam list local | grep debian-13
# Create LXC
pct create 130 local:vztmpl/debian-13-standard_13.0-1_amd64.tar.zst \
--hostname grimmory \
--ostype debian \
--unprivileged 0 \
--cores 1 --memory 2048 --rootfs local-lvm:16 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp \
--onboot 1 \
--mp0 /mnt/library,mp=/mnt/library \
--features nesting=1
pct start 130
pct exec 130 -- apt-get update -qq
```
Set the static IP directly in PVE (same pattern as all other LXCs — no Fritz!Box reservation needed):
```bash
pct set 130 --net0 name=eth0,bridge=vmbr0,ip=192.168.8.213/24,gw=192.168.8.1
pct reboot 130
```
---
## Step 3 — Bootstrap LXC 130
```bash
pct exec 130 -- bash -c '
# Media group
groupadd -g 10000 media
# Docker
apt-get install -y ca-certificates curl
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
'
```
---
## Step 4 — Deploy Grimmory compose
```bash
pct exec 130 -- mkdir -p /opt/grimmory/mariadb/config /opt/grimmory/data /opt/grimmory/bookdrop
```
Write `/opt/grimmory/docker-compose.yml` on LXC 130:
```yaml
services:
grimmory:
image: ghcr.io/grimmory-tools/grimmory:latest
container_name: grimmory
restart: unless-stopped
ports:
- "192.168.8.213:6060:6060"
volumes:
- ./data:/app/data
- /mnt/library/books:/books
- ./bookdrop:/bookdrop
environment:
- DATABASE_URL=jdbc:mariadb://mariadb:3306/grimmory
- DATABASE_USERNAME=grimmory
- DATABASE_PASSWORD=${GRIMMORY_DB_PASSWORD}
- USER_ID=0
- GROUP_ID=10000
- TZ=Europe/Berlin
- FORCE_DISABLE_OIDC=false
extra_hosts:
- "auth.hubris.network:192.168.8.175"
depends_on:
mariadb:
condition: service_healthy
mariadb:
image: lscr.io/linuxserver/mariadb:11.4.8
container_name: grimmory-mariadb
restart: unless-stopped
volumes:
- ./mariadb/config:/config
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=grimmory
- MYSQL_USER=grimmory
- MYSQL_PASSWORD=${GRIMMORY_DB_PASSWORD}
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
```
Write `/opt/grimmory/.env` on LXC 130 (fill real passwords):
```
GRIMMORY_DB_PASSWORD=<same_password_as_booklore>
MYSQL_ROOT_PASSWORD=<root_password>
```
Start:
```bash
pct exec 130 -- bash -c 'cd /opt/grimmory && docker compose up -d mariadb'
# wait ~15s for MariaDB to init, then start grimmory
pct exec 130 -- bash -c 'cd /opt/grimmory && docker compose up -d'
```
Verify Grimmory responds (before DB restore — will show setup wizard):
```bash
curl -s -o /dev/null -w '%{http_code}' http://192.168.8.213:6060
# expect 200 or 302
```
---
## Step 5 — Restore Booklore DB
```bash
# Stop Grimmory (keep MariaDB running)
pct exec 130 -- docker stop grimmory
# Copy dump to LXC 130
pct push 130 /root/booklore-$(date +%Y%m%d).sql /tmp/booklore.sql
# Restore (replace <PASSWORD>)
pct exec 130 -- docker exec -i grimmory-mariadb \
mysql -u grimmory -p<GRIMMORY_DB_PASSWORD> grimmory \
< /tmp/booklore.sql
# Restart Grimmory
pct exec 130 -- docker start grimmory
```
Verify books appear:
```bash
curl -s http://192.168.8.213:6060 | grep -i grimmory
```
---
## Step 6 — Authentik OIDC update
In Authentik Admin UI (`https://auth.hubris.network`):
1. Providers → find `Booklore` provider
2. Edit:
- Name: `Grimmory`
- Client Type: **Public** (Grimmory uses PKCE — no secret needed)
- Redirect URIs: `https://books.hubris.network/oauth2-callback`
- Scopes: openid, profile, email, offline_access
- Back-channel logout URL: `http://192.168.8.213:6060/api/v1/auth/oidc/backchannel-logout`
3. Note the **Client ID** and **Application slug** for Grimmory's OIDC settings
In Grimmory Admin UI (`http://192.168.8.213:6060` → Settings → Authentication → OIDC):
- Issuer URI: `https://auth.hubris.network/application/o/<slug>/` (trailing slash required!)
- Client ID: (from Authentik)
- Client Secret: leave blank (PKCE)
- Click **Test Connection** — all checks should pass (container reaches Authentik via extra_hosts)
---
## Step 7 — Caddy cutover
In the `dtoro/caddy-conf` repo, update `books.hubris.network`:
```caddy
books.hubris.network {
reverse_proxy 192.168.8.213:6060
}
```
Git push → Caddy webhook auto-reloads (see [caddy (121)](../containers/121-caddy.md)).
Test:
```bash
curl -s -o /dev/null -w '%{http_code}\n' https://books.hubris.network
```
---
## Step 8 — Enroll as homelab client
```bash
homelab client add grimmory --lan-ip 192.168.8.213
```
Commits the `age_pubkey` back to `inventory.yaml`.
---
## Step 9 — Verify end-to-end
- [ ] `https://books.hubris.network` loads Grimmory
- [ ] OIDC login via Authentik works
- [ ] Library books from `/mnt/library/books` are visible
- [ ] Reading progress / metadata from Booklore is present
---
## Step 10 — Decommission Booklore on LXC 105
1. Portainer → navigate to the Booklore stack → Stop → Remove
2. Keep the dump at `/root/booklore-<date>.sql` on hubris (or archive to `/mnt/library/documents/`)
---
## Rollback
If something goes wrong before Caddy cutover: no user-visible impact, just shut down LXC 130.
If Caddy already cut over: revert the `books.hubris.network` block to `192.168.8.205:6060` and push. Booklore still running on LXC 105 until Portainer stack is removed.