Media & Entertainment AWS

Grimmory 3.5.0 on AWS User Guide

| Product: Grimmory 3.5.0 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying Grimmory 3.5.0 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened SRS Streaming Media Server & Media Servers AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other media and streaming products in the listing are described on the listing's guide hub.

Grimmory is a free, open source, self-hosted digital library (AGPL-3.0 licence). Point it at your EPUB, PDF, MOBI, comic and audiobook files and it organises them into libraries and shelves, fetches cover art and metadata, and gives every reader their own account, shelves and reading or listening progress. A built-in browser reader and audiobook player handle the common formats, and an OPDS catalogue plus Kobo and KOReader sync carry the same library onto an e-reader.

What is on the image:

  • Grimmory 3.5.0, the official upstream image ghcr.io/grimmory-tools/grimmory, and MariaDB 11.4.12 (ghcr.io/linuxserver/mariadb), both pinned by their linux/amd64 manifest digests and pulled at build time (your instance never pulls an image)
  • Docker CE from Docker's official repository (its signing key pinned by fingerprint), configured so that neither container can reach the instance metadata service
  • nginx serving HTTPS on port 443 with a certificate created on your instance; port 80 redirects to HTTPS
  • A per-instance administrator created on first boot through Grimmory's one-time setup, before the web server opens, so the setup page is never open to the internet
  • A dedicated 64 GiB data volume at /var/lib/grimmory for the database, your books and the watched import folder
  • The complete Grimmory 3.5.0 source code on the instance, as the AGPL-3.0 requires (Step 10)
  • grimmory-set-hostname (serve Grimmory under your own DNS name) and grimmory-selftest (an end-to-end check)

Default user: ubuntu · Architecture: x86_64 · Root volume: 24 GiB gp3 · Data volume: 64 GiB gp3

Security posture

  • No default login and no open setup page. Upstream Grimmory creates its first administrator through a setup page that answers anyone until an account exists. On this image, first boot claims the admin account through that page with a password created on your instance, closes the page and proves the login, all before nginx accepts a connection. The admin password (24 characters) is written only to /root/grimmory-credentials.txt (mode 0600, root).
  • Per-instance secrets. The admin password, the database password, the database root password, Grimmory's token signing key, the TLS key and the database itself are all created on your instance. The image ships with an empty data volume. The database root account is one account with this instance's password: the MariaDB image's password-less local root accounts are removed on first boot.
  • The database is private. MariaDB has no host port. It is reachable only from the Grimmory container on a private Docker network; another container on the instance cannot reach it, and nor can anything outside the instance. Grimmory's own port 6060 listens on the instance's loopback interface only, so nginx is the only way in.
  • The containers cannot read instance metadata. A rule in Docker's DOCKER-USER chain, applied before dockerd starts any container, drops traffic to 169.254.169.254 and fd00:ec2::254, so neither container can fetch the instance's IAM role credentials. Docker, Grimmory's unit and nginx also carry systemd IPAddressDeny for those addresses.
  • HTTPS from first boot. TLS 1.2 and 1.3 only (TLS 1.1 is refused) and Strict-Transport-Security. The host name comes from configuration, never from a request: first boot reads the instance's public DNS name from EC2 instance metadata (IMDSv2) and pins nginx to it; requests for any other host name get their connection closed. nginx sets no cookies of its own.
  • Cross-site requests are refused. nginx refuses state-changing requests and WebSocket upgrades whose Origin or Referer is another site (403), and sends no cross-origin (CORS) headers to other sites.
  • Off by default: the API documentation (OpenAPI and Swagger), header-based remote login and unsafe single sign-on hosts. Forged X-Forwarded-For, X-Real-IP and Remote-User headers from a client never reach Grimmory.
  • Verified build. Both image digests were scanned for known vulnerabilities and their licences checked before the build. SSH allows keys only, no root login and no X11 forwarding; the AMI requires IMDSv2; SSH host keys and the machine ID are generated fresh on every instance.

Prerequisites

  1. An AWS account and permission to launch EC2 instances
  2. An EC2 key pair in the Region you launch in

Recommended instance type: m5.large (2 vCPUs, 8 GiB). Grimmory's Java service and MariaDB together use about 1 GiB at rest; library scans, cover extraction and many readers at once want the headroom.

Connecting to your instance

Version Operating system SSH user
Grimmory 3.5.0 on Ubuntu 24.04 Ubuntu 24.04 LTS ubuntu

Step 1: Launch the instance

Option A: AWS Marketplace console

  1. Open the cloudimg Hardened SRS Streaming Media Server & Media Servers AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  2. Once the subscription is active, choose Launch your software.
  3. Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose Grimmory 3.5.0 on Ubuntu 24.04, then choose your Region.
  4. Pick an instance type (m5.large recommended) and your key pair. The 64 GiB data volume is part of the image; you can make it larger in the storage settings.
  5. Security group: the listing's default group opens SSH (22), HTTP (80) and HTTPS (443). Restrict SSH to your own IP address and restrict 80 and 443 to the networks your readers use. Never open 6060 or 3306.
  6. Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.

Option B: AWS CLI

Run this on your own workstation. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDRs with your own:

SG=$(aws ec2 create-security-group --group-name grimmory \
  --description "Grimmory" --vpc-id vpc-0123456789abcdef0 \
  --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 443 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 80 --cidr <your-mgmt-cidr>
aws ec2 run-instances \
  --image-id ami-0123456789abcdef0 \
  --instance-type m5.large \
  --key-name my-key-pair \
  --security-group-ids "$SG" \
  --subnet-id subnet-0123456789abcdef0 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=grimmory}]'

You do not need --metadata-options: the AMI already makes IMDSv2 required. Associate an Elastic IP if you want the address to survive a stop and start.

Step 2: Connect and find your login

Connect as ubuntu with the key pair you chose:

ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>

First boot takes one to two minutes after the instance starts (a little longer on the very first start, while EC2 reads the container images from their snapshot). It sets the name Grimmory is served as (the instance's public DNS name when it has one), creates the TLS certificate and this instance's passwords, starts the database and Grimmory, claims the administrator and then opens nginx. The login is in a root-only file; this shows the address, the user names and the field names without printing any password:

sudo grep -E '^grimmory\.(url|admin\.user|admin\.email|db\.name|db\.user)=' /root/grimmory-credentials.txt
sudo grep -oE '^[a-z.]+=' /root/grimmory-credentials.txt
sudo stat -c '%a %U:%G %n' /root/grimmory-credentials.txt
grimmory.url=https://ec2-34-203-216-16.compute-1.amazonaws.com/
grimmory.admin.user=admin
grimmory.admin.email=admin@localhost.localdomain
grimmory.db.name=grimmory
grimmory.db.user=grimmory
grimmory.url=
grimmory.admin.user=
grimmory.admin.pass=
grimmory.admin.email=
grimmory.db.name=
grimmory.db.user=
grimmory.db.pass=
grimmory.db.root.pass=
600 root:root /root/grimmory-credentials.txt
Value What it is for
grimmory.url The address of this instance's Grimmory web interface
grimmory.admin.user / grimmory.admin.pass The administrator's sign-in (admin)
grimmory.admin.email A placeholder address for the administrator; change it in Grimmory's settings
grimmory.db.user / grimmory.db.pass Grimmory's own database account (grimmory), used in Step 9
grimmory.db.root.pass The MariaDB root password, for database administration only

To read the passwords, run sudo cat /root/grimmory-credentials.txt in your own session and store them in your password manager.

Step 3: Confirm Grimmory is running

The first-boot unit has finished; Docker, the metadata block, the Grimmory stack and nginx are active; exactly two containers run, from the pinned images. The only public listening sockets are sshd and nginx on 22, 80 and 443; Grimmory's 6060 is on 127.0.0.1 only and MariaDB has no host port at all:

systemctl is-active grimmory-firstboot docker cloudimg-docker-imds-deny grimmory nginx
sudo ss -Htlnp | awk '{print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u
sudo docker ps --format '{{.Names}} {{.Image}} {{.Ports}}'
sudo docker image ls --digests --format '{{.Repository}}:{{.Tag}} {{.Digest}}'
active
active
active
active
active
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:6060 users:(("docker-proxy"))
127.0.0.53%lo:53 users:(("systemd-resolve"))
127.0.0.54:53 users:(("systemd-resolve"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
grimmory ghcr.io/grimmory-tools/grimmory 127.0.0.1:6060->6060/tcp
grimmory-mariadb ghcr.io/linuxserver/mariadb 3306/tcp
ghcr.io/grimmory-tools/grimmory:v3.5.0 sha256:83597bf02da48a8ec4042fd4d4f58b9631f5b131d6dee7d2fcc5cf3f2eec2526
ghcr.io/linuxserver/mariadb:11.4.12-r0-ls224 sha256:8898bac7a45b35856d6973574200131f061f3f996058d5e9e1197f2e6e47d11b

Step 4: Prove the login and the web defaults

Signed-out requests to the API are refused without a cookie being set, the setup page reports that it is closed, the health check answers, plain HTTP redirects to HTTPS, an unknown Host header gets no answer and TLS 1.1 is not offered. The last command is the image's own round-trip check: it signs in with this instance's administrator over https://<your-name>/ and proves that wrong passwords and forged tokens are refused:

H=$(sudo sed -nE 's#^grimmory\.url=https://([^/]+)/?.*#\1#p' /root/grimmory-credentials.txt)
for p in /api/v1/users/me /api/v1/books /api/v1/libraries /api/v1/version; do
  printf '%-18s %s\n' "$p" "$(curl -sk -o /dev/null -w '%{http_code}' --connect-to ::127.0.0.1: "https://$H$p")"
done
curl -sk -D - -o /dev/null --connect-to ::127.0.0.1: "https://$H/api/v1/users/me" | grep -ci '^set-cookie' || true
curl -sk --connect-to ::127.0.0.1: -w '\n' "https://$H/api/v1/setup/status" | python3 -I -c 'import json,sys; d=json.load(sys.stdin); print("setup closed:", d["data"])'
curl -sk --connect-to ::127.0.0.1: -w '\n' "https://$H/api/v1/healthcheck" | python3 -I -c 'import json,sys; d=json.load(sys.stdin)["data"]; print("health:", d["status"], d["version"])'
curl -s -o /dev/null -w 'http://           %{http_code} -> %{redirect_url}\n' --connect-to ::127.0.0.1: "http://$H/"
curl -sk -o /dev/null -w 'unknown Host      %{http_code}\n' -H 'Host: evil.example' https://127.0.0.1/ || true
curl -sk -o /dev/null -w 'TLS 1.1           %{http_code}\n' --tls-max 1.1 --connect-to ::127.0.0.1: "https://$H/" || true
sudo grimmory-selftest
/api/v1/users/me   401
/api/v1/books      401
/api/v1/libraries  401
/api/v1/version    401
0
setup closed: True
health: UP v3.5.0
http://           301 -> https://ec2-34-203-216-16.compute-1.amazonaws.com/
unknown Host      000
TLS 1.1           000
round-trip OK https://ec2-34-203-216-16.compute-1.amazonaws.com/: 6 protected routes 401 anonymously with no Set-Cookie; forged JWT 401; wrong admin password 401; admin login over 443 200 and users/me admin; setup closed 403; DB app + root passwords authenticate, a wrong one refused; v3.5.0

--connect-to ::127.0.0.1: sends each request to this instance while keeping the real host name in the request, exactly as a browser would send it. The 0 is the number of cookies a signed-out request receives. 000 means the server closed the connection without an answer.

Step 5: Sign in to Grimmory

Browse to grimmory.url (https://<public-dns-name>/). The certificate is self-signed, so your browser warns once; accept it for this host, or install your own certificate (Step 8). Sign in with admin and the grimmory.admin.pass value from the credentials file:

The Grimmory 3.5.0 sign-in page served over HTTPS

A new instance has no library yet; Step 6 adds one. Under Settings (the gear at the bottom of the sidebar), Users lists the accounts. The image creates one, admin, as the administrator. Change its e-mail address there, and create an account for each reader with only the permissions they need:

Grimmory's user management with the single admin account

Step 6: Add your books

Grimmory reads books from /books inside its container, which is /var/lib/grimmory/books on the data volume, and imports anything dropped into /bookdrop (/var/lib/grimmory/bookdrop). Copy your files in and give 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

Then create a library in the web interface (Create Library, folder /books), or through the API. This example downloads a public-domain book from Project Gutenberg, signs in as the administrator and creates a library that watches the folder. The password goes to curl through a file descriptor and the session token through a curl config file on a descriptor, so neither appears on a command line, in the shell history or in a log:

sudo -u grimmory curl -fsSL -o /var/lib/grimmory/books/pride-and-prejudice.epub https://www.gutenberg.org/ebooks/1342.epub3.images
H=$(sudo sed -nE 's#^grimmory\.url=https://([^/]+)/?.*#\1#p' /root/grimmory-credentials.txt)
AP=$(sudo sed -n 's/^grimmory\.admin\.pass=//p' /root/grimmory-credentials.txt)
TOKEN=$(curl -sk --connect-to ::127.0.0.1: -H 'Content-Type: application/json' \
  --data-binary @<(printf '{"username":"admin","password":"%s"}' "$AP") \
  "https://$H/api/v1/auth/login" | python3 -I -c 'import json,sys; print(json.load(sys.stdin)["accessToken"])')
unset AP
curl -sk --connect-to ::127.0.0.1: -K <(printf 'header = "Authorization: Bearer %s"\n' "$TOKEN") \
  -H 'Content-Type: application/json' -o /dev/null -w 'create library %{http_code}\n' \
  --data-binary '{"name":"Books","paths":[{"path":"/books"}],"watch":true}' "https://$H/api/v1/libraries"
sleep 20
curl -sk --connect-to ::127.0.0.1: -K <(printf 'header = "Authorization: Bearer %s"\n' "$TOKEN") \
  "https://$H/api/v1/books" | python3 -I -c '
import json, sys
books = json.load(sys.stdin)
print("books:", len(books))
for b in books:
    m = b.get("metadata") or {}
    print("-", m.get("title"), "/", ", ".join(m.get("authors") or []))'
unset TOKEN
create library 200
books: 1
- Pride and Prejudice / Jane Austen

The new library appears in the sidebar and the book, with the cover Grimmory extracted from the file, under Recently Added on the dashboard:

Grimmory's dashboard with the new Books library and the first book under Recently Added

Open the library to browse and filter it, and open a book to read it in the browser; Grimmory keeps each reader's position:

The Grimmory library view of the Books library with its first book

Grimmory also serves an OPDS catalogue and supports Kobo and KOReader sync (Settings), so the same library reaches an e-reader. Fetching metadata from providers such as Google Books, Open Library or Hardcover needs outbound internet access from the instance.

Step 7: The data volume

The database, the books, Grimmory's covers and cache, and the import folder live on the 64 GiB data volume, mounted by its filesystem UUID:

findmnt -n -o SOURCE,FSTYPE,SIZE,TARGET /var/lib/grimmory
sudo ls -l /var/lib/grimmory
grep ' /var/lib/grimmory ' /etc/fstab | sed -E 's/UUID=[0-9a-f-]+/UUID=<uuid>/'
/dev/nvme1n1 ext4   62.4G /var/lib/grimmory
total 32
drwxr-x--- 2 grimmory grimmory  4096 Nov  7 22:56 bookdrop
drwxr-x--- 2 grimmory grimmory  4096 Nov  7 22:56 books
drwxr-x--- 2 grimmory grimmory  4096 Nov  7 22:56 data
drwx------ 2 root     root     16384 Nov  7 21:05 lost+found
drwxr-x--- 4 grimmory grimmory  4096 Nov  7 22:56 mariadb
UUID=<uuid> /var/lib/grimmory ext4 defaults,nofail 0 2

To grow the library's space, enlarge the volume in the EC2 console (Volumes, Modify volume), then run sudo resize2fs on the device that findmnt shows. Snapshot the volume to back the library up (Maintenance, below).

Step 8: Serve Grimmory under your own DNS name

Point a DNS record (for example books.example.com) at the instance's public or Elastic IP address, then tell the image to serve that name. It creates a new self-signed certificate for the name, pins nginx and the cross-site guard to it, updates grimmory.url in the credentials file and keeps the name across reboots:

sudo grimmory-set-hostname books.example.com

For a trusted certificate, obtain one for that name (for example with certbot's certonly mode using its own temporary web server, or from your organisation's CA), copy the certificate chain and key over /etc/ssl/grimmory/grimmory.crt and /etc/ssl/grimmory/grimmory.key (key mode 0600, root), and run sudo systemctl reload nginx.

Step 9: Check the security controls

The DOCKER-USER rules drop the metadata addresses (IPv4 and IPv6); neither container gets an answer from instance metadata, while the Grimmory container still reaches the internet (the 200 is that positive control); the database port answers nothing from the host; the services carry IPAddressDeny; and SSH runs with the cloudimg pins. The last command shows the database's accounts, signing in as root with this instance's root password passed through the environment rather than the command line:

sudo iptables -S DOCKER-USER
sudo ip6tables -S DOCKER-USER
sudo docker exec grimmory sh -c 'wget -q -T 5 -O /dev/null http://checkip.amazonaws.com/ && echo "container -> internet: 200" || echo "container -> internet: failed"'
sudo docker exec grimmory sh -c 'wget -q -T 5 -O /dev/null http://169.254.169.254/latest/meta-data/ 2>/dev/null && echo "grimmory -> IMDS: answered" || echo "grimmory -> IMDS: blocked"'
sudo docker exec grimmory-mariadb curl -s -m 5 -o /dev/null -w 'mariadb -> IMDS token: %{http_code}\n' -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' http://169.254.169.254/latest/api/token || true
DIP=$(sudo docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' grimmory-mariadb)
curl -s -m 5 -o /dev/null -w 'host -> mariadb :3306: %{http_code}\n' "telnet://$DIP:3306" || true
systemctl show -p IPAddressDeny docker grimmory nginx
sudo sshd -T | grep -E '^(passwordauthentication|permitrootlogin|kbdinteractiveauthentication|x11forwarding) '
sudo bash -c 'MYSQL_PWD=$(sed -n "s/^grimmory\.db\.root\.pass=//p" /root/grimmory-credentials.txt) docker exec -e MYSQL_PWD grimmory-mariadb mariadb -u root -t -e "SELECT user, host FROM mysql.user ORDER BY user"'
-N DOCKER-USER
-A DOCKER-USER ! -i br-grimmory -o br-grimmory -m conntrack --ctstate NEW -j DROP
-A DOCKER-USER -d 168.63.129.16/32 -j DROP
-A DOCKER-USER -d 169.254.169.254/32 -j DROP
-N DOCKER-USER
-A DOCKER-USER -o br-grimmory -j DROP
-A DOCKER-USER -d fd00:ec2::254/128 -j DROP
container -> internet: 200
grimmory -> IMDS: blocked
mariadb -> IMDS token: 000
host -> mariadb :3306: 000
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32

IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32

IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
permitrootlogin no
passwordauthentication no
kbdinteractiveauthentication no
x11forwarding no
+-------------+-----------+
| User        | Host      |
+-------------+-----------+
| grimmory    | %         |
| mariadb.sys | localhost |
| root        | %         |
+-------------+-----------+

Step 10: The AGPL-3.0 source offer

Grimmory is licensed under the GNU Affero General Public License version 3. Section 13 requires that anyone using the program over a network is offered the corresponding source of the running version. The instance carries it, so no network access is needed:

head -12 /usr/share/doc/grimmory/SOURCE-OFFER.txt
cd /usr/share/grimmory/source && sha256sum -c grimmory-3.5.0-source.tar.gz.sha256
Grimmory 3.5.0 - AGPL-3.0 offer of corresponding source
================================================================

This appliance runs Grimmory 3.5.0 from the official upstream container image, unmodified:

    ghcr.io/grimmory-tools/grimmory@sha256:83597bf02da48a8ec4042fd4d4f58b9631f5b131d6dee7d2fcc5cf3f2eec2526
    (multi-arch index sha256:bf6fe21c6e247597f7869664a440b5a34242e2d2940575cae39c17bfaa66dc0b, tag v3.5.0)

Grimmory is free software licensed under the GNU Affero General Public License, version 3 (AGPL-3.0-only).
Under section 13, every user who interacts with this program over a network is offered the complete
corresponding source of the version that is running.

grimmory-3.5.0-source.tar.gz: OK

cloudimg makes no changes to Grimmory itself. The image adds only the Docker Compose project, the systemd units, the nginx configuration and the first-boot setup.

Maintenance

  • Change the admin password: in Grimmory, open your profile (top right) and change the password; then replace the old one in /root/grimmory-credentials.txt once you have stored the new one.
  • Operating system updates: unattended security upgrades are on. Reboot when /var/run/reboot-required exists; the metadata block, the Grimmory stack and nginx start again by themselves.
  • Grimmory updates: the stack runs the pinned images and never pulls one. To move to a newer release, launch the newer version of this image when the listing offers it and move the data volume (or a snapshot of it) across with Grimmory stopped. Do not run docker image prune -a: grimmory.service starts the pinned images only.
  • Backups: snapshot the data volume, or dump the database with this instance's own password passed through the environment: sudo bash -c 'MYSQL_PWD=$(sed -n "s/^grimmory\.db\.pass=//p" /root/grimmory-credentials.txt) docker exec -e MYSQL_PWD grimmory-mariadb mariadb-dump -u grimmory grimmory' > grimmory.sql.
  • Logs: sudo docker logs grimmory and sudo docker logs grimmory-mariadb for the stack; /var/log/nginx/ for web requests; sudo journalctl -u grimmory-firstboot for first boot.

Troubleshooting

  • The page does not load right after launch: first boot is still running. sudo journalctl -u grimmory-firstboot -f shows its progress.
  • Browser shows "connection closed": nginx answers only the configured name and closes the connection for any other name. Use grimmory.url, or run sudo grimmory-set-hostname <name> for a new name. Use the name rather than an IP address in the browser too: the cross-site guard accepts changes only from the configured name.
  • "A data conflict occurred" when signing in: Grimmory's session tokens are unique per second, so two sign-ins for the same account in the same second collide. Wait a second and sign in again.
  • "Too many failed login attempts": Grimmory limits failed sign-ins per address and per user name for fifteen minutes. Wait, then use the password from the credentials file.
  • 502 Bad Gateway: Grimmory is starting (it applies its database migrations at start) or has stopped. sudo systemctl status grimmory and sudo docker logs --tail 50 grimmory show why.
  • Uploads of large files fail: nginx accepts request bodies up to 4 GB. If you put a load balancer or another proxy in front, raise its limits too.
  • The end-to-end check: sudo grimmory-selftest signs in with this instance's administrator and prints round-trip OK when everything works.

Licensing

Grimmory is licensed under the GNU Affero General Public License version 3 (AGPL-3.0-only); the licence text is on the instance in /usr/share/doc/grimmory/LICENSE with upstream's NOTICE, and its source is in /usr/share/grimmory/source/ (Step 10). MariaDB Server is licensed under the GNU General Public License version 2. Grimmory is a name of the Grimmory project; cloudimg is not affiliated with, sponsored by or endorsed by the Grimmory project, and the name identifies the software this image contains.

Support

cloudimg provides 24/7 technical support for this image by email and live chat: deployment, first login, HTTPS and DNS setup, libraries and imports, e-reader sync, storage, upgrades and troubleshooting. Contact support@cloudimg.co.uk.