I had ebooks on a laptop, audiobooks in an app that wanted a subscription, and comics in a folder I had stopped opening. BookOrbit collapses all three into one self-hosted library with a browser reader, an OPDS catalogue, and two way progress sync with Kobo and KOReader. It is AGPL-3.0 and Docker only.
This is the setup I actually ran, on a TerraMaster NAS rather than a VM, plus the one permissions mistake that will have you convinced your paths are wrong when they are fine.
Why the NAS and not a VM
My first plan put it on a Linux VM and mounted the library over CIFS. Moving it to the NAS deleted three problems at once.
- No network mount at all. The books already live on the NAS disk. The VM plan needed a CIFS mount, stored credentials, and systemd hardening so that a NAS hiccup could not hang the VM at boot. A container on the box that owns the files needs none of that.
- Smaller blast radius. BookOrbit falling over does not touch anything else I run.
- Space. Terabytes free on the NAS against tens of gigabytes on the VM.
One honest caveat. If your NAS also holds backups, you have just added a workload to the thing that protects everything else. BookOrbit itself is light, under two gigabytes with Postgres, but a runaway library can fill a volume. Keep an eye on free space.
What you need first
Docker on the NAS, which on TerraMaster TOS means installing Docker Engine first and then Docker Manager on top of it. Synology and QNAP have equivalents. Then check over SSH:
docker --version
docker compose version
If docker compose is missing from the CLI, you can still deploy: Docker Manager has a Projects feature that takes a docker-compose.yml and an .env directly.
Create a shared folder for the library, give your user read and write, and make the layout:
mkdir -p /Volume1/Books/{Ebooks,Audiobooks,Comics,_dock}
The setup
Two services, the app and Postgres. The full docker-compose.yml, an .env.example and a setup script are in the download at the end of this post, so I will not paste all of it here. The script creates the directories, generates the four secrets, detects your real UID and GID, and writes the .env with mode 600.
# dry run first, it prints what it would do and changes nothing
BOOKS=/Volume1/Books STACK=/Volume1/docker/bookorbit ./setup.sh
BOOKS=/Volume1/Books STACK=/Volume1/docker/bookorbit ./setup.sh --apply
cd /Volume1/docker/bookorbit
docker compose config --quiet && echo "compose valid"
docker compose up -d
docker compose ps
You want the app running and the database healthy. Then browse to the app, paste the SETUP_BOOTSTRAP_TOKEN the script printed, and create the administrator account. That token is single use and stops working afterwards.
Two details in that compose file worth knowing rather than copying blindly. The app container ships read_only: true with cap_drop: ALL, because upstream has already hardened it, so leave that alone. And the Postgres volume must be on local disk. Books can live on a network share. The database cannot, because Postgres on a network filesystem risks corruption.
The trap: a scan that succeeds and finds nothing
This is the single most common failure with this app and it does not look like a permissions problem. The scan runs, reports success, and your library is empty. You check the paths. The paths are correct. You check them again.
The cause is PUID and PGID not matching whoever owns the library folder. On a NAS the admin user is frequently not uid 1000, which is what every tutorial assumes.
id # your real uid and gid
ls -ln /Volume1/Books # who actually owns the library
Put the real numbers in the .env and bring the stack back up. That is why the setup script detects them rather than letting you guess.
A related one: when you create a library in the UI, the folder you enter is the path as the container sees it. That is /books/Audiobooks, not the host path. The host path appears exactly once, in BOOKS_HOST_PATH.
Naming your library so it scans correctly
The scanner infers author, series and title from folder names. One folder per book, always:
Author - Series NN - Title
Bram Stoker - Dracula <- standalone, no series part
Rebecca Yarros - Empyrean 01 - Fourth Wing
Rebecca Yarros - Empyrean 3.5 - Threshing Day <- novellas keep the decimal
Sarah J. Maas - ACOTAR 01 - A Court of Thorns and Roses
Four rules, each of which I learned by having it wrong:
-
Never leave a loose file directly in the media folder. In Folder as Book mode it has no folder to belong to. A single
.m4bstill gets its own folder. -
Never nest a series folder.
Crescent City/1 - House of Earth and Bloodmakes the scanner read the parent as one book containing every volume. Flatten it and put the series in the folder name. -
Keep the separator exactly space, hyphen, space.
Author- TitleandAuthor-Titleparse differently, and you will end up with all three variants by accident. -
Zero-pad series numbers.
01not1, or book 10 sorts before book 2.
Drop edition noise like (m4b) or Unabridged. It is not part of the title and it confuses the metadata match.
Folder as Book versus File as Book
Make one library per media type, because the allowed formats and metadata priority differ. The setting that matters most is the scan mode.
-
Folder as Book keeps everything in a book's folder together: alternate formats, covers, sidecar
.opfmetadata, and every audio track of one audiobook. It understands disc folders likeCD 1andDisc 2. Use this for audiobooks and for any ebook you keep in more than one format. - File as Book means one file equals one title. Only correct for a flat pile of single-file EPUBs.
If an audiobook appears as thirty separate titles, the library is in File as Book. That is the entire diagnosis.
Reading and listening on a phone
There is an official iOS app, BookOrbit: The Official App, free with in-app purchases. It is offline first, streams or downloads audiobooks with chapter navigation, does text to speech, and has Apple Watch support for offline listening. It needs a BookOrbit server to point at, so it is a client and not a standalone reader.
On Android there is no official app yet. The web PWA plays audiobooks but without proper OS integration, so lockscreen controls and background playback are the weak points. Worth knowing before you promise someone in your house that this will work on their phone.
For e-readers, BookOrbit exposes an OPDS catalogue that KOReader, Thorium and Moon+ Reader can subscribe to. OPDS is ebook oriented, so it is not much use for audiobooks.
Away from home, the honest answer is not to expose the NAS. Reach it over a mesh VPN such as Tailscale and browse to its address on the tailnet, for example http://100.101.102.103:3000.
What I left out on purpose
-
kokorotext to speech. Upstream ships it behind attsprofile. It is CPU heavy and a NAS processor is usually modest, so it is omitted. Add it later withdocker compose --profile tts up -dand watch your load average. - Reverse proxy and HTTPS. Unnecessary when it is reachable on the LAN and over a VPN.
- OIDC and SSO. Supported, but overkill for a handful of users.
The backup conversation nobody has
If your hypervisor runs nightly backups, those back up guests. They do not back up the NAS. Your books and the BookOrbit database live on the NAS, so unless the NAS has its own backup job, nothing is protecting either of them. Decide that deliberately rather than discovering it.
And dump the database before every update, because BookOrbit runs schema migrations on start, which means rolling the image back is not a rollback once a migration has run:
docker exec bookorbit-db pg_dump -U bookorbit bookorbit > bookorbit-db-$(date +%F).sql
docker compose pull
docker compose up -d
When something is wrong
| Symptom | Cause |
|---|---|
| Scan completes, finds no books |
PUID and PGID do not match the owner of the library folder |
| An audiobook shows as many separate titles | Library is in File as Book mode, switch to Folder as Book |
| One book contains an entire series | A nested series folder, flatten it |
docker: command not found |
Engine not installed, or the SSH session needs a re-login after installing |
| Port already in use | Something else has 3000, check with `ss -lnt |
| Postgres will not start, or is corrupt | Its data directory is on a network share. It must be local disk |
| Containers vanish after a NAS firmware update | The update restarted Docker. {% raw %}restart: unless-stopped should bring them back |
The files below are the compose file, the .env template, the setup script and the naming rules as a separate reference. Every address in them is an example, nothing points at a real host, and there are no secrets to strip because the script generates them on your machine.
Download the files
Both are free, no sign up:
-
bookorbit-nas-bundle.zip contains the
docker-compose.yml, an.env.examplewith every variable documented, thesetup.shthat generates the secrets and detects your UID and GID, plus a README of the four things that will bite you. - bookorbit-naming-rules.md is the folder convention on its own, because that is the part you will want open while you reorganise a library.
Related from the lab
- Self-Host Your Photos with Immich
- NFS vs SMB: Which Protocol Should You Use for Your Homelab Storage?
- How I Access My Entire Homelab from Anywhere with Tailscale
Originally published at codewithromi.com.
Top comments (0)