# 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 and from Portainer compose) pct exec 105 -- docker exec \ mysqldump -u grimmory -p 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= MYSQL_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 ) pct exec 130 -- docker exec -i grimmory-mariadb \ mysql -u grimmory -p 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//` (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-.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.