Skip to content

Automating Grimmory (BookLore) Setup with Ansible (No GUI Required)

How to fully automate Grimmory deployment — including first-run admin creation, library setup, and PocketID OIDC — without ever opening the web UI. Applies to Grimmory v2.3+ (formerly BookLore).

The Problem

Grimmory's first-run experience requires you to:

  1. Open the web UI and create an admin account
  2. Log in and create book libraries
  3. Navigate to settings to configure OIDC (if desired)

For IaC deployments, all of this should happen automatically. Grimmory exposes a full REST API that makes this possible — but there's a major gotcha with OIDC that caught us off guard.

Prerequisites

  • Docker and Docker Compose
  • A MariaDB/MySQL database
  • (Optional) A PocketID instance for OIDC SSO
  • (Optional) Ansible for full automation

Step 1: Deploy the Container

Grimmory's docker-compose is straightforward. Note that OIDC env vars are NOT used (explained in Step 4):

services:
  grimmory:
    image: grimmory/grimmory:latest
    container_name: grimmory
    environment:
      - USER_ID=1000
      - GROUP_ID=1000
      - TZ=America/New_York
      - DATABASE_URL=jdbc:mariadb://your-db-host:3306/${DB_NAME}
      - DATABASE_USERNAME=${DB_USER}
      - DATABASE_PASSWORD=${DB_PASSWORD}
    ports:
      - "6060:6060"
    volumes:
      - ./data:/app/data
      - ./books:/books
      - ./bookdrop:/bookdrop
    restart: unless-stopped

Step 2: Create Admin User via API

Grimmory has a dedicated setup endpoint that's only available when no users exist:

# Check if setup is needed
curl -s http://localhost:6060/api/v1/setup/status
# Returns {"data": false} if no users exist (setup needed)
# Returns {"data": true} if already configured

# Create the initial admin user
curl -sf -X POST "http://localhost:6060/api/v1/setup" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "admin",
    "email": "[email protected]",
    "name": "Admin",
    "password": "your-secure-password"
  }'

Important: Use curl (not Ansible's uri module) for any endpoint that sends a password. Ansible's uri module can mangle passwords during JSON serialization, causing BCrypt hash mismatches on subsequent login. This is a known issue that affects multiple applications.

Step 3: Authenticate and Create Libraries

After creating the admin user, log in to get a JWT token:

# Login
TOKEN=$(curl -sf -X POST "http://localhost:6060/api/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "your-secure-password"}' \
  | jq -r '.accessToken')

# Create a book library
curl -sf -X POST "http://localhost:6060/api/v1/libraries" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Books",
    "paths": [{"path": "/books"}],
    "watch": true,
    "metadataSource": "EMBEDDED"
  }'

# Note: Do NOT create a library for /bookdrop — Grimmory's built-in Bookdrop
# feature handles it automatically. The /bookdrop volume mount is sufficient.
# Files dropped there are staged for metadata review, then moved into a
# library on finalize. Creating a library on /bookdrop bypasses this workflow.

To make this idempotent, check existing libraries first:

curl -s "http://localhost:6060/api/v1/libraries" \
  -H "Authorization: Bearer $TOKEN" | jq '.[].name'

Step 4: Configure OIDC (The Big Gotcha)

This is the most important section. Grimmory stores OIDC configuration in its database (app_settings table), NOT via environment variables.

What doesn't work

Setting these env vars in docker-compose has no effect:

# These do NOTHING — Grimmory ignores them!
- OIDC_ENABLED=true
- OIDC_CLIENT_ID=xxx
- OIDC_CLIENT_SECRET=xxx
- OIDC_ISSUER_URI=https://auth.example.com

These are not Spring Boot properties. Grimmory's AppSettingService.java reads OIDC config from the database via AppSettingKey.OIDC_ENABLED with a default of "false". The env vars are silently ignored.

What works: Settings API

After authenticating, configure OIDC via PUT /api/v1/settings:

curl -sf -X PUT "http://localhost:6060/api/v1/settings" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "name": "OIDC_ENABLED",
      "value": "true"
    },
    {
      "name": "OIDC_PROVIDER_DETAILS",
      "value": {
        "providerName": "PocketID",
        "clientId": "your-client-id",
        "clientSecret": "your-client-secret",
        "issuerUri": "https://auth.example.com",
        "scopes": "openid profile email",
        "claimMapping": {
          "username": "preferred_username",
          "name": "name",
          "email": "email",
          "groups": "groups"
        }
      }
    },
    {
      "name": "OIDC_AUTO_PROVISION_DETAILS",
      "value": {
        "enableAutoProvisioning": true,
        "allowLocalAccountLinking": true,
        "defaultPermissions": [],
        "defaultLibraryIds": []
      }
    },
    {
      "name": "OIDC_FORCE_ONLY_MODE",
      "value": "true"
    }
  ]'

Setting reference

Setting Key Type Public Description
OIDC_ENABLED string Yes "true" or "false"
OIDC_PROVIDER_DETAILS JSON Yes Provider config (issuer, client ID/secret, scopes, claim mapping)
OIDC_AUTO_PROVISION_DETAILS JSON No Auto-create users from OIDC, link local accounts
OIDC_FORCE_ONLY_MODE string Yes "true" hides password login form
OIDC_SESSION_DURATION_HOURS string No Session duration (default: 24)

PocketID Client Configuration

In PocketID, create an OIDC client with:

  • Callback URL: https://grimmory.example.com/oauth2-callback
  • PKCE: Disabled

The callback path is /oauth2-callback — this is a frontend route (not a backend API endpoint). Grimmory uses an SPA flow where the frontend receives the authorization code and posts it to the backend.

Making it idempotent

Check if OIDC is already configured before writing:

OIDC_ENABLED=$(curl -s "http://localhost:6060/api/v1/settings" \
  -H "Authorization: Bearer $TOKEN" | jq -r '.oidcEnabled')

if [ "$OIDC_ENABLED" != "true" ]; then
  # Run the PUT /api/v1/settings call above
fi

Step 5: Force Clean (Database Reset)

For DB-backed apps like Grimmory, wiping the filesystem isn't enough — you need to reset the database too:

# Wipe filesystem data
rm -rf /path/to/grimmory/data

# Reset the database
docker exec mariadb mariadb -u root -e \
  "DROP DATABASE IF EXISTS grimmory;
   CREATE DATABASE grimmory;
   GRANT ALL PRIVILEGES ON grimmory.* TO 'grimmory'@'%';
   FLUSH PRIVILEGES;"

# Restart the container — it will be in first-run state
docker compose up -d --force-recreate

Ansible Automation Summary

The full Ansible playbook follows this sequence:

  1. Fetch secrets from your secret store
  2. (Optional) Reset database if force_clean=true
  3. Deploy the container (no OIDC env vars needed)
  4. Wait for health checkGET /api/v1/healthcheck returns 200
  5. Check setup statusGET /api/v1/setup/status
  6. Create admin userPOST /api/v1/setup with curl (only on first run)
  7. AuthenticatePOST /api/v1/auth/login with curl to get JWT
  8. Create Books libraryPOST /api/v1/libraries (idempotent; Bookdrop is built-in, no library needed)
  9. Configure OIDCPUT /api/v1/settings (idempotent with enabled check)

API Reference

Endpoint Method Auth Purpose
/api/v1/healthcheck GET None Health check
/api/v1/setup/status GET None Check if first-run setup is needed
/api/v1/setup POST None (first run only) Create initial admin user
/api/v1/auth/login POST None Get JWT access token
/api/v1/libraries GET/POST Bearer JWT List/create libraries
/api/v1/settings GET/PUT Bearer JWT (admin) Read/write application settings (including OIDC)
/api/v1/settings/oidc/test POST Bearer JWT (admin) Test OIDC provider connectivity

References