# Configuration Every setting below is optional unless noted - Grimoire boots with sensible defaults and generates its own `SECRET_KEY` on first run. Set the variables you need in your `docker-compose.yml`, under `environment:`. Many of these pin a setting that is otherwise editable in the app. When an environment variable is set, its value wins and the matching field is shown read-only in Settings. --- ## Environment variables | Variable | Default | Description | |---|---|---| | `SECRET_KEY` | auto-generated | JWT signing secret. Leave unset and Grimoire generates a random key on first boot and persists it at `DATA_PATH/secret_key`, reusing it across restarts. Set it explicitly (`openssl rand -hex 32`) if you run multiple replicas that don't share `DATA_PATH`. It also keys the stored OPDS, calendar and share-link tokens, so changing it ends every feed URL and share link as well as every session - see [Security](security.md#feed-and-share-tokens-at-rest). Grimoire refuses to start if it's set to a placeholder published in these docs (`change-me`, `replace-this-with-a-long-random-string`, `grimoire-dev-secret-change-in-production`) - see the [FAQ](faq.md#grimoire-wont-start-and-the-log-mentions-secret_key). | | `WORKERS` | `1` | Number of uvicorn worker processes. Each one is a full copy of the app (roughly 125-190 MB). One serves a typical gaming table - see [Workers](performance.md#workers) for when to raise it to `2`. | | `LIBRARY_PATH` | `/app/library` | Optional path to your library directory inside the container if not mounted at /app/library | | `DATA_PATH` | `/app/data` | Optional path for the database, thumbnails, and search cache inside the container if not mounted at /app/data | | `BASE_URL` | `http://localhost:9481` | Public base URL of this instance. Set this to the URL you use to access Grimoire (e.g. `https://grimoire.example.com`) when running behind a reverse proxy. An `https://` value marks every sign-in cookie `Secure`, so it is never sent over plain HTTP (without it, a cookie is marked `Secure` only when its request arrived over HTTPS or as `X-Forwarded-Proto: https` from a [trusted proxy](security.md)). Also used for absolute links in OPDS feeds, campaign calendar subscription links, the OIDC redirect URI and other places that need a fully-qualified URL. | | `APP_VERSION` | from `VERSION` file | Optional. The version shown in the About dialog. The official images bake this in at build time; everything else reads the `VERSION` file that ships in the repo root, so a source checkout or an extracted release tarball reports itself correctly without setting this. Only needed if you run from a directory without that file, where the version otherwise shows as `unknown`. | | `VALKEY_URL` | - | Optional Redis-compatible cache (e.g. `redis://:@valkey:6379/0`). Holds rendered page and map images, sign-in failure and lockout counters, OIDC sign-in state, the IdP's signing keys and scan status, so give it a password (`--requirepass`) and put it in the URL as shown; the example compose files do. The password is redacted from the logs. | | `PAGE_CACHE_TTL` | `604800` | Optional. Seconds a rendered page stays in the Valkey cache (default 7 days). `0` means no expiry. Ignored when `VALKEY_URL` is unset. | | `PAGE_CACHE_MAX_MB` | `2048` | Optional. Size ceiling for the on-disk rendered-page cache at `DATA_PATH/page_cache`. Trimmed oldest-first at startup and after each library scan. `0` disables the trim and lets it grow without bound. | | `PAGE_RECLAIM_INTERVAL` | `10` | Optional. After this many page renders, Grimoire hands the memory used to decode page images back to the OS. Lower (e.g. `3`) trims peak memory while several people read at once, at a few percent of render speed. `0` disables it. | | `PAGE_IDLE_RELEASE` | `300` | Optional. Seconds with no page renders after which open books are closed and render memory is released, so the server drops back to its idle footprint after a reading session. Render workers exit then too. Books reopen on the next page view. `0` disables it. | | `RENDER_WORKERS` | `1` | Optional. Number of processes that render PDF, EPUB and DjVu pages. A file that crashes the PDF library fails that one page instead of stopping Grimoire. More than one renders different books in parallel on multi-core hosts. `0` renders inside the app process, which saves memory but loses the crash protection. See [Page rendering](performance.md#page-rendering). | | `STARTUP_SCAN` | `true` | Optional. Set to `false` to skip the library scan that runs every time Grimoire starts. A scan only re-reads files that changed, but on a very large library even checking every file takes a while, and a restart for an image update or a setting change rarely brings new files. New files are still picked up as they arrive while the library is [watched for changes](file-management.md#watching-for-changes); otherwise by a manual rescan or a scheduled one (Settings → Maintenance → Scheduled Rescan). Queued OCR and model preview work waits for the next scan either way. See the [FAQ](faq.md#every-restart-starts-a-long-library-scan). | | `LIBRARY_WATCH` | - | Optional. `true` or `false`. Pins whether Grimoire [watches the library for changes](file-management.md#watching-for-changes) and rescans by itself, and shows the setting read-only in Settings → Maintenance. Unset, the setting there applies, and it is on by default. | | `LIBRARY_WATCH_MODE` | - | Optional. `auto`, `events`, or `poll`: how changes are noticed. `events` uses the operating system's file notifications, which are instant but only cover changes made on the machine Grimoire runs on. `poll` looks through the library every `LIBRARY_WATCH_POLL_INTERVAL` seconds, which works on any network share. `auto` polls when the library is on NFS, SMB/CIFS or a FUSE mount and uses events otherwise. Pins the setting in Settings → Maintenance. | | `LIBRARY_WATCH_DELAY` | `10` | Optional. Seconds the library has to stay quiet after a change before it is scanned, so a folder still being copied is scanned once, when it is done. A library that never goes quiet is still scanned five minutes after the first change. | | `LIBRARY_WATCH_POLL_INTERVAL` | `120` | Optional. In poll mode, seconds between checks (minimum `5`). Each check lists every folder and reads every file's size and date, so raise it for a large library on a slow share. | | `OCR_ENABLED` | `true` | Optional. Set to `false` to disable OCR of image-only PDFs even on the OCR-capable image. See [OCR](performance.md#ocr). | | `OCR_LANGUAGES` | `eng` | Optional. Tesseract language codes for OCR, e.g. `eng` or `eng+deu+fra`. Extra languages require their tessdata files to be present (see [OCR](performance.md#ocr)). | | `OCR_CONCURRENCY` | `1` | Optional. Number of scanned books OCR'd in parallel by the background OCR worker. Raise on multi-core hosts with spare CPU; keep at `1` on small boxes. Set to `0` to turn OCR off (same as `OCR_ENABLED=false`). See [Speeding up OCR](performance.md#speeding-up-ocr). | | `OCR_DPI` | `150` | Optional. Resolution scanned pages are rasterized at before OCR (clamped 72–600). Higher = more accurate but slower and more memory per page. See [Speeding up OCR](performance.md#speeding-up-ocr). | | `OCR_PAGE_TIMEOUT` | `120` | Optional. Seconds a single page may take to OCR before it is skipped and the book moves on. Raise it on slow hardware, where a dense scan can legitimately need several minutes per page. `0` means no limit. See [Speeding up OCR](performance.md#speeding-up-ocr). | | `MAX_IMAGE_PIXELS` | _unset_ | Optional. Raises the ceiling on how many pixels an image may have before it is refused as a possible decompression bomb. Unset leaves Pillow's default (~89 MP warn, ~179 MP reject), which is sized for untrusted uploads; a 60x60in battlemap scanned at 300 DPI is 324 MP and is rejected by it, indexing with no thumbnail. Set it only if you have images that large and trust their source. Values below 1,000,000 are raised to it. | | `OPDS_ENABLED` | `false` | Optional, Set to `true` to enable the OPDS catalog. See [OPDS](opds.md). | | `MCP_ENABLED` | `false` | Optional. Set to `true` to serve the MCP endpoint at `/mcp`, so AI assistants such as Claude Code can search your library. It authenticates with API keys, so `API_KEYS_ENABLED` must stay on. See [MCP server](mcp.md). | | `MCP_ALLOWED_HOSTS` | _unset_ | Optional. Extra host names `/mcp` answers under, comma-separated (`nas.lan`, `192.168.1.20:9481`; a bare name allows any port). Loopback and `BASE_URL`'s host are always allowed. Needed when an assistant reaches Grimoire at another address, such as a LAN IP. | | `API_DOCS` | `signed-in` | Who may open the interactive API docs (`/api/docs`, `/api/redoc`) and `/api/openapi.json`: `signed-in` (any signed-in account except campaign guests; a browser's session is enough), `public`, or `off`. | | `BACKUP_DIR` | `DATA_PATH/backups` | Optional. Where backup archives are written. Point this at another mounted volume to keep backups off the main disk. When set, the field is read-only in Settings → Maintenance. See [Backups](backups.md). | | `BACKUP_SCHEDULE` | `off` | Optional. `off`, `hourly`, `daily`, or `weekly`. When set, pins the backup schedule and the control is shown read-only in the UI. See [Backups](backups.md). | | `BACKUP_RETENTION_COUNT` | `0` | Optional. Keep at most this many backups, deleting oldest-first. `0` means unlimited. When set, the field is read-only in the UI. See [Backups](backups.md). | | `BACKUP_RETENTION_GB` | `0` | Optional. Keep at most this many gigabytes of backups in total, deleting oldest-first. `0` means unlimited. When set, the field is read-only in the UI. See [Backups](backups.md). | | `LOG_LEVEL` | `info` | Optional Console/Docker log verbosity: `debug`, `info`, `warning`, `error`, or `critical`. The in-app Logs tab (Settings → Logs) always captures `debug`-level entries regardless of this setting. | | `TZ` | `UTC` | Optional. Timezone for all log timestamps - both console/Docker output and the in-app Logs tab. Use an IANA zone name such as `America/Toronto` or `Europe/Berlin`. Defaults to UTC when unset; an unknown zone name logs a warning and uses UTC. | | `PUID` / `PGID` | owner of the library | Optional. The user and group ids Grimoire runs as inside the container. Unset, it runs as the owner of the mounted library folder, then of the data folder, then `1000:1000`, and stays root only if that user could not reach something root could. Set them (e.g. `99`/`100` on Unraid) to choose; `PUID=0` keeps it running as root. Ignored when the container is started with `--user`. See [Which user Grimoire runs as](docker-install.md#which-user-grimoire-runs-as). | | `UMASK` | inherited | Optional. Octal umask applied to everything Grimoire writes - uploaded files, exported sidecars, thumbnails, and the database (e.g. `022` for `rw-r--r--`, `002` for group-writable, `000` for `rw-rw-rw-`, which is what Unraid users usually want). Useful where you cannot set the process umask yourself, such as Kubernetes. `GRIMOIRE_UMASK` is accepted as an alias. Left unset, Grimoire keeps whatever umask it inherited; an unparseable value is ignored with a warning rather than guessed at. | | `ALLOW_PASSWORD_AUTHENTICATION` | - | Optional, `true` or `false`. When set, pins password authentication on or off and overrides the toggle in Settings → Authentication (the toggle is shown read-only). When unset, the in-app setting is used. First-run admin setup always requires a username and password regardless of this value. | | `API_KEYS_ENABLED` | `true` | Set to `false` to turn [API keys](api.md#api-keys) off for the whole instance, admins included: existing keys stop working, the `/api/api-keys` endpoints refuse, and the UI hides the key menus. Which users may hold keys is set per user in Settings → Users. | | `SHARE_LINKS_ENABLED` | `true` | Set to `false` to turn [share links](sharing.md) off for the whole instance: every existing link stops working (`404`), nobody can create one, and the UI hides the Share buttons. Links are kept, so turning it back on restores them. Who may create links is set in Settings → Users. | | `CORS_ALLOWED_ORIGINS` | _unset_ | Optional. Comma-separated origins, e.g. `https://foundry.example.com`, whose web pages may call the API from the browser - a Foundry VTT module, a browser extension, or a dashboard. Unset sends no CORS headers. Entries must be exact origins (`scheme://host[:port]`); a wildcard or a path is ignored with a warning. Cookies are never allowed cross-origin, so these integrations authenticate with an [API key](api.md#api-keys). Server-side tools like Homepage don't need it. See [Calling the API from a browser](api.md#calling-the-api-from-a-browser-cors). | | `GRIMOIRE_CODEX_DB_API_TOKEN` | - | Optional. A [Grimoire Codex DB](codex.md) API token (`cdx_…`). **Grimoire Codex DB is off and hidden unless this is set.** Every Grimoire Codex DB request uses it, and records sent are credited to the account that owns it. It is read only from the environment and never stored or shown. | | `GRIMOIRE_CODEX_DB_URL` | - | Optional. Pins the Grimoire Codex DB address (default `https://db.grimoirecodex.org`), for a self-hosted Grimoire Codex DB. | | `GRIMOIRE_CODEX_DB_SEND_HASHES` | - | Optional, `true` or `false`. Pins whether book lookups and submissions include the file's SHA-256. Off by default, but `true` is recommended: it matches books exactly - see [What is sent](codex.md#what-is-sent-and-when). | | `GUEST_ACCESS_ENABLED` | - | Optional, `true` or `false`. When set, pins guest invite codes on or off and overrides the toggle in Settings → Authentication (the toggle is shown read-only). When unset, the in-app setting is used. See [Guest invites](campaigns.md#guest-invites). | | `DISABLE_FOLDER_CATEGORY_INFERENCE` | - | Optional, `true` or `false`. When set, pins folder-name category inference on or off and overrides the toggle in Settings → Application (shown read-only). When `true`, books are not auto-assigned a category from their folder names and fall back to `uncategorized`. A per-system `.no-auto-category` marker file disables inference for just that system. | | `DISABLE_EXTERNAL_ADD_ON_INSTALL` | `false` | Optional, `true` or `false`. When `true`, Grimoire never fetches anything from a community repository: wiki note templates, metadata add-ons, and themes alike. The **Browse** tabs disappear and the catalogue endpoints refuse. Writing templates in the app, uploading a `.md`, and pasting a theme still work, so a locked-down or air-gapped server keeps every feature - it just stops fetching. **Replaces `WIKI_TEMPLATES_DOWNLOAD_DISABLED`**, which is no longer read. See [Themes](themes.md) and [Wiki note templates](wiki-templates.md). | | `ADDON_INDEX_CREDENTIALS` | - | Optional. Access tokens for [private add-on sources](addons.md#private-sources), as a JSON object mapping an index URL to a token or to `{"token": "...", "header": "PRIVATE-TOKEN"}`. A token is sent only to URLs inside its source. A pinned source's token is read-only in Settings → Add-ons; tokens for other sources can still be saved there. | | `OIDC_*` env vars | - | Optional. Each OIDC setting (`OIDC_ENABLED`, `OIDC_ISSUER_URL`, `OIDC_TOKEN_ISSUER`, `OIDC_AUTHORIZATION_ENDPOINT`, `OIDC_TOKEN_ENDPOINT`, `OIDC_USERINFO_ENDPOINT`, `OIDC_JWKS_URI`, `OIDC_END_SESSION_ENDPOINT`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_SIGNING_ALG`, `OIDC_BUTTON_TEXT`, `OIDC_BUTTON_BG_COLOR`, `OIDC_BUTTON_TEXT_COLOR`, `OIDC_BUTTON_BORDER_COLOR`, `OIDC_BUTTON_RADIUS`, `OIDC_BUTTON_ICON`, `OIDC_GROUPS_CLAIM`, `OIDC_ADMIN_GROUP`, `OIDC_GM_GROUP`, `OIDC_PLAYER_GROUP`, `OIDC_PERMISSIONS_CLAIM`, `OIDC_MATCH_BY`, `OIDC_AUTO_LAUNCH`, `OIDC_AUTO_REGISTER`) can be pinned via env. When set, the field is read-only in Settings → Authentication. When unset, the in-app value is used. See [OpenID Connect](oidc.md). | | `AUTH_RATE_LIMIT` | `10/minute` | Per-IP throttle applied to the credential-checking endpoints (`/api/auth/login`, `/api/auth/setup`, `/api/auth/guest-login`) and to failed `X-API-Key` attempts on any endpoint. Exceeding it returns `429`. Uses a [`limits`](https://limits.readthedocs.io/en/stable/quickstart.html#rate-limit-string-notation) string like `20/minute` or `100/hour`. See [Security hardening](security.md). | | `RATE_LIMIT_ENABLED` | `true` | Optional. Set to `false` to disable auth rate limiting entirely. | | `ACCOUNT_RATE_LIMIT` | `20/hour` | Optional. Failed sign-ins counted per account (by the username typed) and failed share-link passwords counted per link, whatever address they come from. A browser that has signed in to the account before keeps its own count, so failures elsewhere don't lock its owner out. Same string format as `AUTH_RATE_LIMIT`. | | `PASSWORD_MIN_LENGTH` | `12` | Optional. Minimum length for new account passwords (first-run setup, new users, resets, password changes). Passwords set before a change keep working. Commonly used passwords are refused at any length. Lowering it below the default logs a warning at start-up. | | `SHARE_LINK_PASSWORD_MIN_LENGTH` | `8` | Optional. Minimum length for new share-link passwords. Commonly used passwords are refused at any length. | | `ACCESS_TOKEN_EXPIRE_MINUTES` | `30` | Optional. How long an access token stays valid. This is also the longest a revoked session can keep working, so lowering it tightens revocation at the cost of more background refreshes. See [Sessions and token revocation](security.md#sessions-and-token-revocation). | | `REFRESH_TOKEN_EXPIRE_DAYS` | `30` | Optional. How long a session survives without use before you must log in again. The clock resets on every refresh, so an actively-used session stays alive indefinitely. | | `TRUST_FORWARDED_FOR` | `true` | Optional. When `true`, `X-Forwarded-For` is read from connections that come from a trusted proxy (`TRUSTED_PROXIES`), so each client behind the proxy gets its own rate-limit bucket and shows its own address under Active Sessions. Set to `false` to ignore the header - and `X-Forwarded-Proto` - from everyone; recommended on Docker Desktop and rootless Podman or Docker exposed without a proxy, where every client arrives from a private address. Behind a TLS proxy, set `BASE_URL` to your `https://` address if you turn it off. | | `TRUSTED_PROXIES` | loopback and private ranges | Optional. Comma-separated addresses or CIDR ranges whose `X-Forwarded-For` is read. The default (`127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7`) covers a proxy or tunnel on the same host, Docker network or LAN - nginx, Nginx Proxy Manager, SWAG, Traefik, Caddy, Cloudflare Tunnel, Tailscale - with nothing to set. Add your proxy's address if it reaches Grimoire from a public IP (a VPS, for example). Any client connecting from these ranges is believed too, so a LAN device can name its own address. See [Behind a reverse proxy](security.md#behind-a-reverse-proxy). | ## Volumes ```yaml volumes: # Your library - writable, so you can manage files from inside Grimoire. # Append ":ro" if you would rather Grimoire could not modify it (see below). - /path/to/your/library:/app/library # Persistent data (database, thumbnails, page cache) - grimoire_data:/app/data ``` ### Read-only or writable? Grimoire only writes to your library when you ask it to. Browsing, searching, reading, and metadata editing never touch the files, and no background job rewrites your library. Two features do write, and both are opt-in and admin-only: | Feature | Needs | What it writes | |---|---|---| | [File management](file-management.md) - upload, move, rename, delete, new folders | Writable mount | The files and folders you act on | | [Sidecar export](sidecars.md) - `.opf` / `.nfo` / `.json` | Writable mount | Sidecar files next to your content | | Everything else - browse, search, read, tags, favorites, campaigns, metadata edits | Either | Nothing in the library folder | **Writable (the default)** is the recommended setup for most people: leave the `:ro` suffix off and uploading a new PDF is a drag-and-drop into the browser rather than a trip to the shell. Writes stay confined to the library root, and the destructive ones are guarded - deleting a folder that still holds files makes you type its name first. **Read-only** is opt-in hardening: append `:ro` and the container cannot modify the library at all. Everything except the two features above behaves identically, and nothing half-fails - the file manager browses normally and tells you the library is read-only, file actions on a book's ⋮ menu are not shown, changing a book's category saves the category without moving the file, and sidecar export reports the read-only mount and skips the write while your metadata edits are still saved. The mount is not a one-way decision and nothing in Grimoire's database depends on it. Edit the volume line, `docker compose up -d`, and the write features appear or disappear accordingly. --- ## See also - [Performance](performance.md) - what `OCR_*`, `PAGE_CACHE_*`, and `WORKERS` actually change - [Security hardening](security.md) - the rate-limit, session, and proxy variables in context - [Backups](backups.md) - the `BACKUP_*` variables - [OpenID Connect](oidc.md) - the `OIDC_*` variables - [Docker installation guide](docker-install.md) - where to put them if you are new to Docker