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:
- Open the web UI and create an admin account
- Log in and create book libraries
- 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'surimodule) for any endpoint that sends a password. Ansible'surimodule 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:
- Fetch secrets from your secret store
- (Optional) Reset database if
force_clean=true - Deploy the container (no OIDC env vars needed)
- Wait for health check —
GET /api/v1/healthcheckreturns 200 - Check setup status —
GET /api/v1/setup/status - Create admin user —
POST /api/v1/setupwith curl (only on first run) - Authenticate —
POST /api/v1/auth/loginwith curl to get JWT - Create Books library —
POST /api/v1/libraries(idempotent; Bookdrop is built-in, no library needed) - Configure OIDC —
PUT /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
- Grimmory Website & Docs
- Grimmory GitHub (formerly BookLore)
- AppSettingKey.java — all available settings keys
- OidcProviderDetails.java — OIDC config DTO