Get started
Install
Shelf is one small web app with its data in one folder: a SQLite database, and the e-books, audiobooks, and cover pictures beside it. The Docker image is the easiest way to run it, and has OCR for scanned PDFs built in.
With Docker
docker run -d --name shelf -p 8080:8080 -v shelf-data:/data \
--restart unless-stopped ghcr.io/doodersrage/shelf:latest
Then open http://localhost:8080. Images are published for both amd64 and arm64 (a Raspberry Pi 4 or 5, most NAS boxes, Apple silicon). Every release is also published under its own version, such as ghcr.io/doodersrage/shelf:2.14.0, which is the better choice when you want to decide when to update.
Everything Shelf keeps is under /data in the container:
| Path | What it holds |
|---|---|
/data/shelf.db |
The database: books, readers, notes, highlights, places, and settings. |
/data/ebooks |
E-book files, named for good when saved and never changed. |
/data/audio |
Audiobooks, one folder each. |
/data/covers |
Cover pictures kept on the shelf. |
/data/keys |
The keys that seal sign-in cookies and two-step secrets. Keep this private. |
/data/backups |
Nightly copies of the database. |
With Docker Compose
services:
shelf:
image: ghcr.io/doodersrage/shelf:latest
ports:
- "8080:8080"
volumes:
- shelf-data:/data
# A folder to drop books into; each reader gets a folder inside it (see Import:Folder):
# - /srv/books-inbox:/import
environment:
# Behind a proxy that ends HTTPS (Caddy, nginx, Traefik):
# Hosting__BehindProxy: "true"
# Email for password resets and notifications (Email__Host and Email__From are both needed):
# Email__Host: smtp.example.org
# Email__From: shelf@example.org
# Email__PublicAddress: https://shelf.example.org
# Stop new accounts once yours exists:
# Accounts__AllowSignUp: "false"
# Add books dropped into the folder mounted at /import:
# Import__Folder: /import
Ocr__Languages: eng
restart: unless-stopped
volumes:
shelf-data:
docker compose up -d
Settings are environment variables, with __ where the documentation writes :. Configuration lists them all.
To keep the database in PostgreSQL rather than SQLite, add a database service and point Shelf at it; see PostgreSQL:
services:
shelf:
# … as above, plus:
environment:
Database__Provider: Postgres
ConnectionStrings__Postgres: "Host=db;Database=shelf;Username=shelf;Password=change-me"
depends_on: [db]
db:
image: postgres:17
environment:
POSTGRES_USER: shelf
POSTGRES_PASSWORD: change-me
volumes:
- shelf-db:/var/lib/postgresql/data
restart: unless-stopped
volumes:
shelf-data:
shelf-db:
Tip
Kindle files need Calibre to be turned into EPUBs, which adds several hundred megabytes, so the published image leaves it out. Build your own with it: docker build --build-arg CALIBRE=true -t shelf . The same goes for ffmpeg, which read-along needs only with a speech-to-text service that limits uploads, such as OpenAI's: --build-arg FFMPEG=true.
On Unraid, TrueNAS, or CasaOS
Ready-made templates are in the deploy folder:
- Unraid: in Docker, choose Add Container, and paste
https://raw.githubusercontent.com/doodersrage/shelf/main/deploy/unraid/shelf.xmlinto Template. Data goes to/mnt/user/appdata/shelf, and Shelf runs as Unraid'snobody:users(99:100). - TrueNAS SCALE 24.10 or later: make a dataset for Shelf, then Apps, Discover Apps, Custom App, Install via YAML, and paste
deploy/truenas/docker-compose.ymlwith your dataset's path. It runs as TrueNAS'sappsuser (568). - CasaOS: App Store, Custom Install, import, and paste
deploy/casaos/docker-compose.yml.
Which user Shelf runs as
The image starts as root only long enough to make /data belong to the user Shelf runs as, then switches to that user: PUID and PGID, 1654 (the image's own app user) unless you set them. So a folder from the host, which is often owned by someone else, works as /data without any chown. Give an import folder's owner as PUID and PGID, since Shelf never changes who owns your own files there. Started with --user, the image runs as that user and changes nothing.
Moving from a Docker volume to a folder
Stop Shelf, then copy what is inside the volume to the folder: the volume's own files sit in a _data folder (docker volume inspect shelf-data gives its path), so copy _data's contents, not _data itself. shelf.db, keys, ebooks, audio, and covers should be directly in the folder. Mount the folder as /data and start Shelf; it takes over the copied files whoever copied them.
If Shelf can't use the folder, it says why when it starts: the files are one folder down in _data, or the folder can't be written to. A folder that stays unwritable is mounted read-only (:ro), needs :z on an SELinux host (Fedora, RHEL), or is a network share; keep the database on a local disk, since SQLite can't lock a file on most shares.
The first account
The first visit goes to the sign-in page; choose Make an account. The first account:
- becomes the admin, who can manage the other readers, see the activity log, and take backups;
- takes any books that were on the shelf before there were accounts.
Every later account starts with an empty shelf of its own. Once your household is signed up, you can close sign-up with Accounts:AllowSignUp set to false.
From source
Shelf needs the .NET 10 SDK. For OCR, install Tesseract and Poppler too (see Running on a server).
git clone https://github.com/doodersrage/shelf.git
cd shelf
dotnet run --project src/Shelf.Api
Open http://localhost:5041. The database, ebooks, audio, covers, and keys are made beside the project on first run.
For development there is also an Aspire app host, which starts Shelf with a dashboard of its logs and traces:
dotnet run --project Shelf.AppHost --launch-profile http
Note
On Arch Linux, the dotnet-sdk package does not include ASP.NET Core. Install aspnet-runtime and aspnet-targeting-pack before building.
Updating
Shelf updates its own database when it starts, so updating is replacing the app:
docker pull ghcr.io/doodersrage/shelf:latest
docker stop shelf && docker rm shelf
docker run -d --name shelf -p 8080:8080 -v shelf-data:/data \
--restart unless-stopped ghcr.io/doodersrage/shelf:latest
With Compose, docker compose pull && docker compose up -d. The changelog says what each release changes, including anything to know before updating.
Shelf asks GitHub twice a day whether a newer release is out, and tells admins at the top of every page, with what's new and how to update, until they update or choose Not now for that release. Only the release's number is read; nothing about your shelf is sent. Updates:Check set to false turns it off.
Tip
Take a snapshot from Readers before a big update. It holds the whole server, and restoring it is unpacking it over a stopped shelf. See Backups.
Putting it on the internet
Passwords and device keys travel with every request, so a shelf reached from outside your home needs HTTPS. Put it behind a reverse proxy such as Caddy, nginx, or Traefik, and tell Shelf it is there. Running on a server shows how.