Media & Entertainment AWS

BookLore 2.4.0 on AWS User Guide

| Product: BookLore 2.4.0 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying BookLore 2.4.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 products in the listing are described on the listing's guide hub.

BookLore is a free, self-hosted digital library (GNU AGPL v3). It organises EPUB, PDF and comic (CBZ/CBR) files into libraries and shelves, fetches cover art and metadata, keeps each user's reading progress, and has a built-in reader in the browser. OPDS, Kobo and KOReader sync let you take the same library onto an e-reader.

What is on the image:

  • BookLore 2.4.0, built from the upstream v2.4.0 release (commit f397e98c): the Angular web interface compiled into the same Java application as the API, exactly as the upstream container image is built
  • Browser and Java libraries with published security advisories raised to fixed releases within their lines (Angular 21.2.25, DOMPurify 3.4.17, Tomcat 11.0.27, Jackson 2.22.4 and 3.2.4; see Security posture)
  • Temurin 25 Java runtime (Eclipse Adoptium) and MariaDB 11.4 LTS (MariaDB Foundation), both from their signed apt repositories so they keep receiving security updates
  • BookLore on 127.0.0.1:6060 and MariaDB on 127.0.0.1:3306, behind nginx serving HTTPS on port 443 with a certificate created on your instance; port 80 redirects to HTTPS
  • A per-instance administrator account claimed on first boot, before the site is reachable; BookLore has no default account
  • A dedicated 30 GiB library volume mounted at /var/lib/booklore (books, covers and caches, the auto-import folder)
  • booklore-set-hostname, a helper that serves BookLore under your own DNS name, and booklore-selftest, an end-to-end check

Default user: ubuntu · Architecture: x86_64 · Root volume: 12 GiB gp3 · Library volume: 30 GiB gp3

Project status: in September 2026 the BookLore maintainers announced BookOrbit as BookLore's successor. BookLore remains available in maintenance mode and, in the project's words, "will continue to receive occasional maintenance, bug fixes, and security updates". The web interface shows the same announcement as a dismissible banner.

Security posture

  • No default account, and no setup window. BookLore creates its first administrator through a one-time setup page that is open only while the database has no users. On this image first boot claims that administrator itself (user admin, a 32-character password generated on your instance) while BookLore listens only on loopback, proves the setup page is closed, and only then lets nginx start. Nobody can reach the setup page from the network.
  • Per-instance secrets and database. The image ships no database at all. On first boot MariaDB initialises a fresh data directory on your instance (the database root account uses socket authentication only, with no password), and first boot creates BookLore's database with a password generated on your instance. BookLore then generates its own token-signing key in that database. The TLS key is also generated on your instance. Passwords are passed through root-only temporary files and standard input, never on a command line.
  • Fails closed. BookLore starts only after first boot has written its database password, and nginx only after first boot has finished. If first boot cannot finish, nothing is served.
  • Only nginx faces the network. Exactly ports 22, 80 and 443 listen beyond loopback. BookLore runs as the unprivileged booklore user under a systemd sandbox (read-only system, no home directories, no capabilities, its own /tmp). Its memory is set explicitly from the instance's RAM at every start (40%, between 768 MiB and 4 GiB) rather than the container default of 75%, which would starve the database.
  • HTTPS from first boot. nginx serves TLS 1.2 and 1.3 with a self-signed certificate created on your instance (TLS 1.1 is refused) and sends Strict-Transport-Security.
  • The host name comes from configuration, never from a request. First boot reads the instance's public DNS name (else its public or private IPv4 address) from EC2 instance metadata (IMDSv2) and pins nginx to it. Requests for any other host name get their connection closed, requests to the instance's IP addresses are redirected to that name, and client-sent X-Forwarded-Host headers are dropped, so links BookLore generates (OPDS feeds, Kobo sync) always name your instance.
  • Cross-origin requests. BookLore treats an empty origin list as "allow every origin". This image sets the allowed origin to your instance's own address, so browser requests from other sites are refused. nginx also refuses state-changing requests and WebSocket upgrades from other sites.
  • Client address from the connection. nginx replaces X-Forwarded-For and X-Real-IP with the real peer address and drops Forwarded and Client-IP; BookLore trusts forwarded headers from loopback only, so its login rate limit and audit log see the real client. BookLore's header-based single sign-on (Remote-User and related headers) is off, and nginx drops those headers from every client.
  • The app cannot read instance metadata. BookLore, its first-boot unit, nginx and MariaDB are blocked from the instance metadata service (169.254.169.254, fd00:ec2::254) by systemd IPAddressDeny; only the small unit that looks up the host name at first boot may use it. BookLore fetches cover images and metadata from addresses users supply, so this matters: at build time a cover fetch of the metadata service was refused by BookLore, and a metadata fetch through a code path BookLore does not filter never got an answer.
  • Dependencies. The BookLore 2.4.0 release pins versions with published advisories: Angular's router and DOMPurify in the web interface, and Tomcat (critical) and Jackson in the Java application. This image raises each within its release line and changes none of BookLore's own code. The exact build inputs ship with the source on the instance (see Licensing). Upstream's container also ships a prebuilt unrar binary under a restrictive licence; this image does not, and BookLore reads CBR files with its built-in Java reader instead.
  • Less phone-home. BookLore's audit log looks up the country of every client IP address at ip-api.com over plain HTTP. This image switches that off with one line in /etc/hosts; delete the line to restore it. The rest is documented under Maintenance.
  • Hardened SSH and system. 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. Ubuntu packages are fully patched at build time and unattended security upgrades are on.

Prerequisites

  1. An AWS account and permission to launch EC2 instances
  2. An EC2 key pair in the Region you launch in
  3. Your books (EPUB, PDF, CBZ/CBR and others), which you upload in the browser or copy onto the library volume

Recommended instance type: m5.large (2 vCPU, 8 GiB). BookLore gives Java about 3.2 GiB of heap there; the database and the system use most of the rest.

Connecting to your instance

Version Operating system SSH user
BookLore 2.4.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 BookLore 2.4.0 on Ubuntu 24.04, then choose your Region.
  4. Pick an instance type (m5.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  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.
  6. Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch. The image brings its 30 GiB library volume with it; you can make it larger at 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 booklore \
  --description "BookLore digital library" --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 \
  --block-device-mappings 'DeviceName=/dev/sdf,Ebs={VolumeSize=200,VolumeType=gp3}' \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=booklore}]'

The block device mapping grows the library volume to 200 GiB; leave it out to keep 30 GiB. 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 about a minute after the instance starts. It sets the name BookLore is served as (the instance's public DNS name when it has one), creates the TLS certificate, initialises MariaDB, creates the database and its password, starts BookLore, claims the administrator, and writes /var/lib/cloudimg/booklore-credentials.txt (mode 0600, root). These are the entries in that file (the password is not shown here):

sudo grep -E '^(BOOKLORE_URL|ADMIN_USERNAME|ADMIN_EMAIL)=' /var/lib/cloudimg/booklore-credentials.txt
sudo grep -oE '^[A-Z_]+=' /var/lib/cloudimg/booklore-credentials.txt
sudo stat -c '%a %U:%G %n' /var/lib/cloudimg/booklore-credentials.txt /etc/booklore/secret.env
BOOKLORE_URL=https://ec2-13-218-49-130.compute-1.amazonaws.com/
ADMIN_USERNAME=admin
ADMIN_EMAIL=admin@booklore.invalid
BOOKLORE_URL=
ADMIN_USERNAME=
ADMIN_PASSWORD=
ADMIN_EMAIL=
600 root:root /var/lib/cloudimg/booklore-credentials.txt
640 root:booklore /etc/booklore/secret.env
Value What it is for
BOOKLORE_URL The address of this instance's BookLore
ADMIN_USERNAME Sign-in name of the administrator (admin)
ADMIN_PASSWORD Administrator password, generated on this instance
ADMIN_EMAIL A placeholder address; set your own in BookLore after signing in

To read the password, run sudo cat /var/lib/cloudimg/booklore-credentials.txt in your own session and store it in your password manager. /etc/booklore/secret.env holds the database password BookLore uses, readable only by root and the app.

Step 3: Confirm BookLore is running

The two first-boot units have finished, MariaDB, BookLore and nginx are active, and only sshd and nginx listen beyond loopback (BookLore is on 127.0.0.1:6060, MariaDB on 127.0.0.1:3306, the resolver on 127.0.0.53). The library volume is mounted at /var/lib/booklore (nginx runs one worker per vCPU, so the number of nginx entries follows your instance size):

systemctl is-active booklore-host.service booklore-firstboot.service mariadb.service booklore.service nginx.service
sudo ss -Htlnp | awk '{print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u
head -4 /opt/booklore/VERSION
java -version 2>&1 | head -1
mariadb --version
df -h /var/lib/booklore | tail -1
active
active
active
active
active
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"),("nginx"),("nginx"))
127.0.0.1:3306 users:(("mariadbd"))
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"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"),("nginx"),("nginx"))
[::ffff:127.0.0.1]:6060 users:(("java"))
booklore_version=2.4.0
booklore_tag=v2.4.0
source_commit=f397e98c5ab021b02e8930ce278583af11637238
source_sha256=1ca2a5b3183d74286d96a821b58ecab2c4440835472095e716ec214c3b1c0c25
openjdk version "25.0.5" 2026-10-20 LTS
mariadb from 11.4.13-MariaDB, client 15.2 for debian-linux-gnu (x86_64) using  EditLine wrapper
/dev/nvme1n1     30G  272K   28G   1% /var/lib/booklore

Step 4: Prove the login and the web defaults

The health endpoint reports version 2.4.0, the web interface answers, the API refuses a signed-out request, 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 password, reads a protected page, proves that signed-out and forged-token requests are refused, checks the database password and that a wrong one is refused, and confirms the setup page is closed:

H=$(sed -n 's/^set \$bl_host "\(.*\)";$/\1/p' /etc/nginx/booklore-hostvar.conf)
C=(--connect-to ::127.0.0.1:)
curl -sk "${C[@]}" "https://$H/api/v1/healthcheck"; echo
curl -sk "${C[@]}" -o /dev/null -w 'web interface -> %{http_code} %{content_type}\n' "https://$H/"
curl -sk "${C[@]}" -o /dev/null -w 'API, signed out -> %{http_code}\n' "https://$H/api/v1/books"
curl -s "${C[@]}" -o /dev/null -w 'http -> %{http_code} %{redirect_url}\n' "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 "${C[@]}" -o /dev/null -w 'TLS 1.1 -> %{http_code}\n' --tlsv1.1 --tls-max 1.1 "https://$H/" || true
sudo booklore-selftest
{"data":{"status":"UP","message":"Application is running smoothly.","timestamp":"2026-10-28T01:59:15.110751879","version":"2.4.0"},"message":"Pong","status":200,"timestamp":"2026-10-28T01:59:15.110761786"}
web interface -> 200 text/html;charset=UTF-8
API, signed out -> 403
http -> 301 https://ec2-13-218-49-130.compute-1.amazonaws.com/
unknown Host -> 000
TLS 1.1 -> 000
round-trip OK: administrator admin authenticated over https://ec2-13-218-49-130.compute-1.amazonaws.com/, protected API 200, anonymous 403 and forged-token 401, database role per-instance password OK and wrong password refused, setup closed

--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. 000 means the server closed the connection without an answer.

Step 5: Sign in to BookLore

Browse to BOOKLORE_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_USERNAME and ADMIN_PASSWORD.

The BookLore sign-in page asking for the username and password of the administrator generated on this instance

All Books shows every book with its cover, with filters by author, format, read status and more on the right. The screenshots show a small library of generated placeholder books; your instance starts empty:

The BookLore All Books view with four demo books and their covers, the libraries and shelves on the left, the filter panel on the right and the BookOrbit announcement banner at the top

Open a book and choose Read to read it in the browser; BookLore remembers your position for each user:

A demo EPUB open in the BookLore reader, showing the chapter heading The Long Dusk and its first paragraph

The gear icon in the top bar opens Settings: reader defaults, metadata providers, users, e-mail, OPDS, Kobo and more. Change the administrator's e-mail address and password there first (Users), and add an account for each reader (there is no public sign-up):

The BookLore Settings page on the Reader tab, with tabs for View, Metadata, Application, Users, Email, Patterns, OIDC and more

Step 6: Add your books

There are three ways to add books:

  • Upload in the browser: the upload icon in the top bar takes EPUB, PDF, CBZ/CBR, MOBI/AZW3 and other files (nginx accepts uploads of up to 4 GB).
  • Copy files into a library folder: create a library in BookLore (the + next to Libraries) that points at a folder on the library volume, for example /var/lib/booklore/books/fiction, copy files there and give them to the booklore user. BookLore scans the folder (choose Watch when you create the library to pick up new files automatically).
  • Auto-import folder: files dropped into /var/lib/booklore/bookdrop appear under BookDrop for review and import.
scp -r ~/Books/* ubuntu@<public-ip>:/tmp/books/
sudo install -d -o booklore -g booklore -m 0750 /var/lib/booklore/books/fiction
sudo cp -r /tmp/books/. /var/lib/booklore/books/fiction/
sudo chown -R booklore:booklore /var/lib/booklore/books/fiction

Everything that grows with your library lives on the library volume: books/ (your files), data/ (covers, thumbnails and caches), bookdrop/ and tmp/. To make it bigger, increase the EBS volume's size in the EC2 console and then grow the file system in place:

sudo resize2fs "$(findmnt -no SOURCE /var/lib/booklore)"

Metadata and covers: BookLore can look up metadata and covers from Google Books, Open Library, Amazon, Goodreads, Hardcover and others; these lookups run only when you ask for them (or enable them for a library), and go from your instance to those services.

Step 7: Check the security controls

BookLore, its first-boot unit, nginx and MariaDB carry IPAddressDeny for the metadata addresses, first boot recorded that it could not reach the metadata service from inside its own unit, BookLore runs sandboxed as booklore, cross-origin requests are limited to this instance, header-based sign-on is off, the country lookup is switched off, and SSH runs with the cloudimg pins:

systemctl show -p IPAddressDeny booklore.service booklore-firstboot.service nginx.service mariadb.service
systemctl show -p User -p ProtectSystem -p ProtectHome -p NoNewPrivileges booklore.service
sudo grep -o 'IMDS-REFUSED-IN-FIRSTBOOT.*' /var/log/cloudimg-firstboot.log | tail -1
sudo cat /etc/booklore/host.env
grep -E '^REMOTE_AUTH_ENABLED=' /etc/booklore/booklore.env
grep ip-api.com /etc/hosts
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|x11forwarding) '
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32

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

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

IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32
User=booklore
ProtectHome=yes
ProtectSystem=strict
NoNewPrivileges=yes
IMDS-REFUSED-IN-FIRSTBOOT (http 000; IPAddressDeny in effect)
ALLOWED_ORIGINS=https://ec2-13-218-49-130.compute-1.amazonaws.com
REMOTE_AUTH_ENABLED=false
0.0.0.0 ip-api.com  # cloudimg-booklore-geoip: BookLore audit-log country lookup off (remove this line to enable)
permitrootlogin no
passwordauthentication no
x11forwarding no

Step 8: Serve BookLore 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 allowed origin to it, restarts BookLore, updates BOOKLORE_URL in the credentials file and keeps the name across reboots:

sudo booklore-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/booklore/booklore.crt and /etc/ssl/booklore/booklore.key (key mode 0600, root), and run sudo systemctl reload nginx.

Step 9: E-readers: OPDS, Kobo and KOReader

These features are off until you turn them on in Settings:

  • OPDS (any OPDS reader app): enable the OPDS server, then create an OPDS user with its own password; point the reader app at https://<your-name>/api/v1/opds.
  • Kobo sync: enable Kobo sync for your user; BookLore shows the API address to put on the Kobo.
  • KOReader progress sync: enable KOReader sync for your user and use the shown address and credentials in KOReader's progress-sync plugin.

E-readers need a certificate they trust, so set up your DNS name and certificate first (Step 8).

Maintenance

  • Operating system updates: unattended security upgrades are on, including Temurin and MariaDB from their own repositories. Reboot when /var/run/reboot-required exists; MariaDB, BookLore and nginx start again by themselves.
  • BookLore updates: BookLore is the single file /opt/booklore/app.jar. To move to a newer release, launch the newer version of this image (when the listing offers it) and move the library volume and a database dump across, or follow BookLore's own documentation. BookLore applies its database migrations itself when it starts.
  • Backups: the database is MariaDB's booklore database (sudo mariadb-dump booklore > booklore.sql as root uses socket authentication) and the books, covers and caches are on the library volume. An EBS snapshot of the library volume plus a database dump is a complete backup.
  • Memory: to set the Java heap yourself, add BOOKLORE_HEAP_MB=2048 (for example) to /etc/booklore/booklore.env and run sudo systemctl restart booklore.
  • Logs: sudo journalctl -u booklore for the app, sudo journalctl -u mariadb for the database, /var/log/nginx/ for web requests, /var/log/cloudimg-firstboot.log for first boot.
  • What the instance contacts: with the country lookup off, BookLore contacts other services only when you use a feature that needs them: metadata and cover lookups, Kobo sync (which forwards unknown requests to Kobo's store), OIDC sign-in once you configure it, and e-mail once you configure SMTP. The version panel in the web interface asks api.github.com for the latest release. The operating system contacts the AWS Systems Manager agent endpoints, Ubuntu's update servers and the time service; disable the SSM agent if you do not use Session Manager.

Troubleshooting

  • The web page does not load right after launch: first boot is still running. sudo journalctl -u booklore-host.service -u booklore-firstboot.service -f shows its progress.
  • Browser shows "connection closed": nginx answers only the configured name and redirects the instance's own IP addresses to it. Use BOOKLORE_URL, or run sudo booklore-set-hostname <name> for a new name.
  • "Too many failed login attempts": BookLore locks a user name or address for 15 minutes after five wrong passwords. Wait, then sign in with the password from the credentials file.
  • A sign-in occasionally fails with "A data conflict occurred": two sign-ins for the same user in the same second collide in BookLore; sign in again.
  • New files do not appear: check they are owned by booklore (Step 6) and rescan the library from its menu; sudo journalctl -u booklore -n 50 shows the scan.
  • The end-to-end check: sudo booklore-selftest prints round-trip OK when everything works.

Licensing

BookLore is licensed under the GNU Affero General Public License v3. As that licence requires for software used over a network, the complete corresponding source of the BookLore that runs on your instance is on the instance itself: the unmodified upstream release archive, the cloudimg build inputs (the two web-interface dependency files and the Gradle script that raise the libraries listed above) and the build steps, with checksums. The licence text and the written source offer are in /usr/share/doc/booklore/:

cd /usr/share/booklore/source && sha256sum -c SHA256SUMS
booklore-2.4.0-source.tar.gz: OK
cloudimg-build-inputs/BUILD.txt: OK
cloudimg-build-inputs/booklore-ui-package-lock.json: OK
cloudimg-build-inputs/booklore-ui-package.json: OK
cloudimg-build-inputs/cloudimg-pins.init.gradle: OK

The source of this release is also at github.com/booklore-app/booklore/tree/v2.4.0. MariaDB is licensed under the GPL v2 and the Temurin runtime under the GPL v2 with the Classpath Exception. cloudimg is not affiliated with, endorsed by or sponsored by the BookLore project or its maintainers.

Support

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