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:
131
plans/done/2026-06-01-slate-ax-to-sodola-migration.md
Normal file
131
plans/done/2026-06-01-slate-ax-to-sodola-migration.md
Normal 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 3–5.
|
||||
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/`.
|
||||
318
plans/done/2026-06-04_130000-deprecate-claudio-bot.md
Normal file
318
plans/done/2026-06-04_130000-deprecate-claudio-bot.md
Normal 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
|
||||
443
plans/done/2026-06-25-yuvomi-deployment.md
Normal file
443
plans/done/2026-06-25-yuvomi-deployment.md
Normal 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
|
||||
```
|
||||
246
plans/done/2026-06-29-grimmory-migration.md
Normal file
246
plans/done/2026-06-29-grimmory-migration.md
Normal 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.
|
||||
Reference in New Issue
Block a user