Problem: docs-lint (added in the wiki-hq reorg) surfaced 126 broken relative
links that predated this session — a container rename, incident/plan docs
that moved into archive/done subfolders without their inbound links being
updated, and a handful of relative-depth bugs in files nested under
containers/archive/ and plans/done/.
Fixes applied, by category:
- 124-authentik.md -> 106-auth-outpost.md (container was renamed; ~40 refs).
- investigations/{2026-04-21-hubris-crash-loop,2026-05-31-authentik-vps-migration}.md
-> archive/ prefix (both moved to investigations/archive/ previously).
- plans/{2026-06-01-slate-ax-to-sodola-migration,2026-06-04_130000-deprecate-claudio-bot,
2026-06-25-yuvomi-deployment}.md -> plans/done/ prefix.
- Depth bugs in files nested one level deeper than their siblings assumed
(investigations/archive/*, knowledge/wiki/containers/archive/*,
plans/done/*) — corrected relative-path depth.
- Destroyed containers with no surviving page (126-plato) delinked to the
containers/index.md archaeology row instead of a 404.
- ludo-mini.yaml -> strong.yaml (host was renamed, same physical machine).
- netbird-vps.md (no narrative page exists) -> netbird-vps.yaml (substrate
record, matching the existing convention for hosts without a wiki page).
- runbook-dpkg-interrupted.md refs -> .agents/skills/runbook-dpkg-interrupted/SKILL.md
(missed in the phase-4 runbook move because the referencing files used a
bare filename, not a runbooks/ prefix).
- One dangling forward-reference to a never-written investigation delinked
to the actual incident record it was describing.
Left alone: two links in knowledge/wiki/containers/101-jellyfin.md into
devops/homelab-authentik-admin/ — an intentional reference to a sibling repo,
not present in this checkout.
Verification: broken-link count 126 -> 2 (real remainder is the cross-repo
reference above); gen-topology.py --check still exit 0; build_host_files.py
still idempotent; all inventory.yaml doc_page targets still resolve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
247 lines
6.4 KiB
Markdown
247 lines
6.4 KiB
Markdown
# 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)](../../knowledge/wiki/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.
|