Deployment guide

Grimmory

Overview

Grimmory is an open-source, self-hosted digital library for people and teams with a serious collection of ebooks, comics and audiobooks. Point it at your EPUB, PDF, MOBI, comic and audiobook files, and it organises them into libraries and shelves and fetches cover art and rich metadata. Every member of your household or organisation gets their own account, shelves, and reading and listening progress.

A built-in browser reader and audiobook player handle the core formats, with bookmarks and per-user position tracking. An OPDS catalogue plus Kobo and KOReader sync let you carry the same library onto a dedicated e-reader.

This SecureAnyCloud image runs Grimmory the way upstream recommends, from its official container image, on a single Azure VM with a dedicated data volume for the library.

What you get:

  • Grimmory 3.3.1 from the pinned official image ghcr.io/grimmory-tools/grimmory:v3.3.1. The application is a Spring Boot service with the Angular web console built in, listening on 127.0.0.1:6060
  • grimmory.service, the Docker Compose stack: the app on 127.0.0.1:6060 and a bundled MariaDB on a private container network
  • A bundled MariaDB 11.4 container for library, user and progress data, with no host port
  • An nginx.service reverse proxy on TCP 80, hardened and tuned for Grimmory's large headers, streaming uploads and progress websockets, and ready for TLS
  • grimmory-firstboot.service, which generates every per-instance secret and claims the administrator on first boot
  • A dedicated 64 GiB data volume mounted at /var/lib/grimmory for the database, books, audiobooks, covers and the auto-import folder
  • The complete AGPL-3.0 corresponding source, shipped on the instance
  • An Ubuntu 24.04 LTS base with the latest patches and unattended security upgrades enabled
  • 24/7 SecureAnyCloud support with a 24-hour response SLA

Architecture. The application and a bundled MariaDB run as a two-service Docker Compose project, with a hardened nginx reverse proxy in front on port 80. The database is reachable only on a private container network, never on the host. The application's own port is published to loopback only, so nginx is the sole ingress.

The database, book and audiobook files, generated covers and the watched auto-import folder all live on the data volume mounted at /var/lib/grimmory. The library grows on its own disk, and you can expand or snapshot that disk independently of the operating system.

Secure by default. Grimmory creates its first administrator through a one-time setup call that the API accepts only while no users exist. Upstream therefore ships no default account at all, rather than a vendor password to rotate.

This image goes further. The shipped image contains an empty database with no credential of any kind. On the first boot of every instance, a one-shot service generates a unique database password and administrator password and brings the bundled database up so that it initialises with those per-instance secrets. It then claims the administrator through the same one-time endpoint before nginx ever serves port 80, and proves that the new credentials authenticate, that a forged token is rejected and that the setup endpoint is closed.

Prerequisites

You need an active Azure subscription, an SSH key, and a VNet with a subnet.

Recommended VM size: Standard_B2s (2 vCPU, 4 GB RAM). The whole stack is memory-tuned to run comfortably in 4 GB with no swap at all. The Java heap is capped instead of claiming a percentage of RAM, and MariaDB's buffers are capped to match.

Move to a larger size if you plan to import very large collections or serve many concurrent readers.

1

Deploy from the Azure Portal

Search the Azure Marketplace for Grimmory, choose the plan, and create the VM.

In the networking step, attach an NSG that allows inbound TCP 22 (SSH) and TCP 80 (the web console) from your client networks only. For production, put a TLS reverse proxy in front of port 80.

No other port needs to be open. The bundled MariaDB stays on a private container network, and the application's own port 6060 is published to loopback only, so nginx is the only ingress.

2

Deploy from the Azure CLI

RG="grimmory-prod"; LOCATION="eastus"; VM_NAME="grimmory-01" IMAGE_URN="sac1647283583153:grimmory-ubuntu-24-04:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION" az network vnet create -g "$RG" --name gr-vnet --address-prefix 10.114.0.0/16 --subnet-name gr-subnet --subnet-prefix 10.114.1.0/24 az network nsg create -g "$RG" --name gr-nsg az network nsg rule create -g "$RG" --nsg-name gr-nsg --name allow-ssh --priority 100 \
  --source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp az network nsg rule create -g "$RG" --nsg-name gr-nsg --name allow-web --priority 110 \
  --source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 80 --access Allow --protocol Tcp
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:grimmory-ubuntu-24-04:default:latest
az vm create -g "$RG" --name "$VM_NAME" --image "$IMAGE_URN" \
  --size Standard_B2s --storage-sku StandardSSD_LRS \
  --admin-username azureuser --ssh-key-values "$SSH_KEY" \
  --vnet-name gr-vnet --subnet gr-subnet --nsg gr-nsg --public-ip-sku Standard

The library data disk is part of the image, so Azure creates and mounts it for you automatically. There is no extra disk to attach.

3

Connect via SSH

ssh azureuser@<vm-ip>

All services start automatically on first boot, after grimmory-firstboot.service has generated this instance's secrets, brought the bundled database up and claimed the administrator. First boot also brings the container stack up for the first time, so it typically completes within a minute or two of the VM reporting ready.

4

Verify the services

Check that the services are running:

sudo systemctl is-active grimmory nginx
sudo test -f /var/lib/sac/data/grimmory-firstboot.done && echo FIRSTBOOT_DONE
sudo ss -tlnH | awk '{print $1, $4}' | sort -u

Expected output: both units are active and first boot has completed. Only SSH and nginx are on a public address. The application port stays on loopback, and the database is on no host port at all.

active active FIRSTBOOT_DONE LISTEN 0.0.0.0:22 LISTEN 0.0.0.0:80 LISTEN 127.0.0.1:6060 LISTEN 127.0.0.53%lo:53 LISTEN [::]:22 LISTEN [::]:80

In the output:

  • The application is published on 127.0.0.1:6060, so it is reachable only from the VM itself.
  • The port 53 entry is the local systemd-resolved DNS stub.
  • The bundled MariaDB appears nowhere in the list, because it lives on a private container network with no host port.
  • nginx is the sole ingress.

To see the two containers, run sudo docker compose -f /opt/grimmory/docker-compose.yml ps.

5

Retrieve your administrator credentials

Every instance generates its own administrator password and database password on first boot and writes them to a root-only file. Read it with:

sudo ls -l /root/grimmory-credentials.txt
sudo cat /root/grimmory-credentials.txt

The file is mode 600, owned by root. It contains the console URL, the administrator username and password, and the database credentials. Sign in with those values, then change the administrator e-mail address and password under Settings > Users.

6

Confirm there is no default login

Grimmory creates its first administrator through a one-time setup endpoint that the API accepts only while no users exist. This image claims that administrator during first boot, before nginx starts, so nobody can reach your instance and claim it first.

Confirm that the setup endpoint is closed:

curl -s http://127.0.0.1/api/v1/setup/status

Expected output: setup has already been completed on this instance.

{"data":true,"message":"Initial setup has already been completed.","status":200,"timestamp":"..."}

A protected endpoint must refuse both an anonymous caller and a forged token:

curl -s -o /dev/null -w 'anonymous: %{http_code}\n' http://127.0.0.1/api/v1/users/me
curl -s -o /dev/null -w 'forged token: %{http_code}\n' http://127.0.0.1/api/v1/users/me \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJub2JvZHkifQ.not-a-valid-signature'

Expected output:

anonymous: 401 forged token: 401
7

Check the application and the library volume

Run the commands below:

curl -s http://127.0.0.1/api/v1/healthcheck df -h /var/lib/grimmory | tail -1 free -m | head -2 swapon --show

The output shows the following:

  • The health endpoint reports the running version.
  • The dedicated 64 GiB library volume is mounted at /var/lib/grimmory.
  • The whole stack sits comfortably inside 4 GB.
  • swapon --show prints nothing, because the image ships with no swap at all.
8

Sign in to the console

Browse to http://<vm-ip>/ and sign in with the administrator username and password from the credentials file.

9

Add your books

Create a library from the Libraries section of the sidebar and point it at the container library folder /books. On the host, this is the data volume path /var/lib/grimmory/books.

Copy your EPUB, PDF, MOBI, comic or audiobook files there. Grimmory scans them, extracts titles, authors and covers, and shelves them.

sudo install -d -o grimmory -g grimmory -m 0750 /var/lib/grimmory/books ls -ld /var/lib/grimmory/books /var/lib/grimmory/bookdrop

Then copy your own files in and hand them to the grimmory user. Replace the source path with your own:

sudo cp ~/my-collection/*.epub /var/lib/grimmory/books/
sudo chown -R grimmory:grimmory /var/lib/grimmory/books

You can also drop files into the watched auto-import folder (the container path /bookdrop, on the host /var/lib/grimmory/bookdrop), or upload them straight from the browser with the upload button in the toolbar.

The All Books view shows the shelved collection with covers. The filter panel lets you narrow by author, format, read status, publisher, language or year.

10

Read in the browser

Click any book to open it in the built-in reader. EPUB, PDF and comic formats all render in the browser, with per-user reading position, bookmarks and a progress indicator. You can stop on one device and resume on another.

Audiobooks open in the built-in player with the same per-user position tracking.

11

Settings, users and sync

Settings exposes reader and player preferences per format, metadata provider configuration, user administration, e-mail delivery, OIDC single sign-on and the task manager.

Create an account for each reader under Settings > Users and give each one only the permissions they need. Each user keeps their own shelves and progress.

Grimmory also serves an OPDS catalogue and supports Kobo and KOReader sync, so the same library is reachable from a dedicated e-reader.

Metadata fetching from providers such as Amazon, Google, Open Library and Hardcover requires outbound internet access from the VM. It is optional and disabled until you configure it.

12

Server components

  • Application: ghcr.io/grimmory-tools/grimmory:v3.3.1, Spring Boot on Temurin JDK 25, published on 127.0.0.1:6060
  • Web console: Angular, built into the same image and served by the application
  • Database: bundled MariaDB 11.4 container (grimmory-mariadb) on a private container network, with database and role grimmory
  • Reverse proxy: nginx.service on TCP 80, hardened (server tokens off, host guard, sensitive paths denied)
  • Compose project: /opt/grimmory/docker-compose.yml, managed by grimmory.service
  • Library volume: 64 GiB disk at /var/lib/grimmory (config/ = database, data/, books/, bookdrop/)
  • Secrets file: /opt/grimmory/.env (mode 600, generated at first boot)
  • Credentials: /root/grimmory-credentials.txt (mode 600)
  • Corresponding source: /usr/share/grimmory/source/ and /usr/share/doc/grimmory/

The Java heap is capped at 768 MB and MariaDB's buffer pool at 128 MB, so the two never contend on a 2 vCPU instance. If you move to a larger VM and want to raise the heap, edit JDK_JAVA_OPTIONS in /opt/grimmory/docker-compose.yml, then run sudo systemctl restart grimmory.

13

Review the AGPL-3.0 source offer

Grimmory is licensed under the GNU Affero General Public License version 3. Section 13 requires that anyone who interacts with the program over a network is offered the corresponding source of the running version. This instance satisfies that on disk, with no network access required:

cat /usr/share/doc/grimmory/SOURCE-OFFER.txt cd /usr/share/grimmory/source && sha256sum -c grimmory-3.3.1-source.tar.gz.sha256

The complete unmodified v3.3.1 source tree is shipped as a release tarball at /usr/share/grimmory/source/. The licence text is at /usr/share/doc/grimmory/LICENSE. The written offer names the exact running image digest and all of the paths.

SecureAnyCloud makes no modifications to Grimmory itself. This image supplies only the Docker Compose project, systemd units, a hardened nginx vhost and the first-boot credential bootstrap.

14

Configuration, logs and backups

Run a quick health check of the whole stack:

sudo systemctl is-active grimmory nginx
sudo nginx -t

For deeper investigation, read the service journals and the container logs. They print a lot of output, so run them on their own:

sudo systemctl status grimmory
sudo journalctl -u grimmory-firstboot --no-pager sudo docker logs --tail 100 grimmory
sudo docker compose -f /opt/grimmory/docker-compose.yml ps

The stack reads its per-instance secrets from /opt/grimmory/.env. After you edit the compose project, restart it with sudo systemctl restart grimmory.

Database maintenance runs inside the bundled MariaDB container and reads this instance's password straight from the credentials file, so there is nothing to type or paste:

sudo docker exec grimmory-mariadb mariadb -u grimmory \
  -p"$(sudo sed -n 's/^grimmory\.db\.pass=//p' /root/grimmory-credentials.txt)" \
  grimmory -e "SELECT COUNT(*) AS tables FROM information_schema.tables WHERE table_schema='grimmory';"

To back up the library, snapshot the data volume and dump the database the same way:

sudo docker exec grimmory-mariadb mariadb-dump -u grimmory \
  -p"$(sudo sed -n 's/^grimmory\.db\.pass=//p' /root/grimmory-credentials.txt)" \
  grimmory > ~/grimmory-backup.sql

Troubleshooting

The console does not load. Check that grimmory.service is active and that first boot completed: sudo systemctl is-active grimmory && sudo journalctl -u grimmory-firstboot --no-pager. On first boot, the stack pulls nothing, because the images are already cached in the image. It brings the bundled database up and applies its schema migrations, which takes a minute or two on a Standard_B2s.

A sign-in attempt returns "A data conflict occurred". Grimmory issues a refresh token whose validity timestamps have one-second resolution, and stores it under a uniqueness constraint. Two sign-ins for the same account inside the same second therefore collide. Wait a second and sign in again.

Sign-in returns "Too many failed login attempts". Grimmory rate-limits five failed attempts per source address and per username within fifteen minutes. Wait for the window to pass, or retrieve the current password from /root/grimmory-credentials.txt.

Uploads or large books fail. The nginx vhost allows request bodies up to 4 GB and streams them straight through. If you front the instance with another proxy or a load balancer, raise the equivalent limits there too.

Metadata lookups return nothing. Metadata providers are third-party services reached over the internet. Confirm that the VM has outbound access and that the provider is enabled under Settings > Metadata.

Support

24/7 technical support is included with this image. Contact sales@secureanycloud.com for help with deployment, library and shelf organisation, metadata providers, OPDS, Kobo and KOReader sync, bulk imports, user administration, TLS and custom domains, version upgrades, performance tuning and database administration.

Deploy on Azure

Launch Grimmory on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure