Files
oikos/plans/done/2026-06-29-grimmory-migration.md
dtoro 4efddb8bed docs: fix pre-existing broken links surfaced by docs-lint
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>
2026-07-06 17:53:35 +02:00

6.4 KiB

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

# 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

# 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):

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

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

pct exec 130 -- mkdir -p /opt/grimmory/mariadb/config /opt/grimmory/data /opt/grimmory/bookdrop

Write /opt/grimmory/docker-compose.yml on LXC 130:

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:

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):

curl -s -o /dev/null -w '%{http_code}' http://192.168.8.213:6060
# expect 200 or 302

Step 5 — Restore Booklore DB

# 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:

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:

books.hubris.network {
  reverse_proxy 192.168.8.213:6060
}

Git push → Caddy webhook auto-reloads (see caddy (121)).

Test:

curl -s -o /dev/null -w '%{http_code}\n' https://books.hubris.network

Step 8 — Enroll as homelab client

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.