




The Ultimate Guide for EPUB & Audiobooks Self-Hosting
Hey y’all! I’ll keep updating this until I’m satisfied with it. :)
Why did I create this post?
Well, I kinda spent a whole week setting up my self-hosted reading stack, and I thought this might save people some time and be a good starting point for anyone looking into building something similar.
What is this, and what’s the workflow?!
First: Server-side
This guide will take you through a full self-hosted reading stack using:
- BookOrbit https://github.com/bookorbit/bookorbit
- BookBridge https://github.com/cporcellijr/bookbridge
- Storyteller https://storyteller-platform.dev/docs/installation/self-hosting
- Audiobookshelf (ABS) https://github.com/advplyr/audiobookshelf
- Double Commander https://github.com/linuxserver/docker-doublecommander
Second: Client-side
This will guide you through:
- Installing KOReader on your Kindle/Kobo
- iOS/android phone/tablet apps: BookOrbit, Storyteller, and Prologue
- Read-aloud books
- Apple Watch
How everything fits together
SELF-HOSTED READING STACK
=========================
YOUR BOOK
EPUB + AUDIOBOOK/M4B
|
|
v
+---------------------------+
| BOOKORBIT |
| |
| Main library / metadata |
| EPUBs + M4B + Read-Aloud |
| Web reader / player |
+---------------------------+
| | |
| | |
EPUB + M4B| | | BookOrbit library
| | |
v | v
+----------------+ | +--------------------+
| STORYTELLER | | | DOUBLE COMMANDER |
| | | | |
| Creates synced | | | Creates hardlinks |
| Read-Aloud EPUB| | | without duplicating|
+----------------+ | | the actual data |
| | +--------------------+
| | |
| | | Hardlink
| | v
| | +-------------------+
| | | AUDIOBOOKSHELF |
| | | (ABS) |
| | | |
| | | Audiobook library |
| | | M4B conversion |
| | +-------------------+
| | |
| | |
+-----------+--------------+
|
v
+----------------------+
| BOOKBRIDGE |
| |
| Syncs reading/listen |
| progress between |
| different formats |
+----------------------+
CLIENT DEVICES
==============
+------------------+ +-------------------------+
| Kindle / Kobo | | iPhone / iPad |
| | | |
| KOReader | | BookOrbit App |
| BookOrbit Plugin | | Storyteller App |
+--------+---------+ | Prologue -> ABS |
| +------------+------------+
| |
+--------------+---------------+
|
v
Read the same library everywhere
with progress/highlights syncing
where supported.
Workflow
BookOrbit
BookOrbit will be your main library manager.
This is where I keep my EPUBs, audiobooks, and Read-Aloud books. It can fetch metadata and covers when needed, and it can automatically process files dropped into its Book Dock folder.
It can also act as a web reader and audiobook player.
Install the BookOrbit app on your iPhone/iPad and you can read books, listen to audiobooks, and use Read-Aloud books directly from your phone.
Install the BookOrbit plugin on your Kindle/Kobo running KOReader and you can browse your library directly from your e-reader, download books, and sync things like reading progress, highlights, annotations, and reading stats back to your BookOrbit server.
Pretty cool! ...
Warning! as i was informed by people in this thread that the dev of bookorbit had previously developed an app called booklore and that app was mainly AI slob with no one steering so i just want people to know! ill still continue using bookorbit as no other bookmanager yet have all the features of bookorbit! to know more booklore
BookBridge
Let’s say you have three versions of the same book:
- EPUB
- Audiobook
- Read-Aloud book
You might read the EPUB on your e-reader, then later want to continue by listening to the audiobook on your phone, then later switch to the Read-Aloud version.
Normally, every time you switch formats you would have to manually find where you left off.
BookBridge solves this.
It aligns the ebook and audiobook so progress can be translated between the different formats.
So you can stop reading on your e-reader, pick up your phone later, start the audiobook, and continue around the same point — and vice versa.
It uses a small local model during the initial processing/alignment of the book, rather than needing to continuously process it while you read.
Storyteller
Storyteller creates a Read-Aloud EPUB.
This is basically an EPUB containing the text of the book together with its audiobook narration. While the narration plays, the corresponding text can be highlighted and followed along with.
These Read-Aloud books are also supported by BookOrbit, so you can store and use them as part of the same library.
Storyteller works with EPUB 3 for this process. If the book you have is EPUB 2, Storyteller can convert it to EPUB 3 first.
It also performs the initial alignment between the EPUB text and audiobook when creating the Read-Aloud edition.
Audiobookshelf
Audiobookshelf (ABS) is another book/audiobook manager.
Honestly, if you are already running BookOrbit, you might not need ABS at all.
I personally keep it in my stack for two main reasons:
- It has very good audiobook conversion tools. For example, it can take a folder containing multiple MP3 files and convert them into a single clean M4B audiobook with chapters.
- I like some of the dedicated audiobook clients that work with ABS, especially Prologue on iPhone.
Double Commander
We will also run Double Commander, which is basically a file manager for power users.
I mainly use it because it provides an easy GUI for creating hardlinks.
Why is that useful?
We are running two book managers: BookOrbit and Audiobookshelf.
I don’t want to copy the same 1 GB audiobook twice (and those are expensive with all the AI shenanigan):
BookOrbit library -> 1 GB
AND
Audiobookshelf library -> another 1 GB
That would waste storage space.
Instead, we can create a hardlink.
A hardlink is not really a shortcut. Both filenames point to the same underlying file data/inode on the filesystem.
So you can have something like:
/books/bookorbit/Author/Book/Book.m4b
/books/audiobookshelf/Author/Book/Book.m4b
Both appear to be normal files to their respective applications, but the audiobook data is only stored once on disk.
Deleting one hardlink does not delete the actual data as long as another hardlink to that file still exists.
This is extremely useful when multiple self-hosted applications need to see the same large media files.
Docker
I’m going to provide example Docker Compose YAML for all the services mentioned above.
You should edit the paths, user IDs, passwords, domain names, timezone, etc. for your own setup.
Hopefully these examples make getting started easier. Obviously I can’t provide an example for every possible configuration.
My own server runs TrueNAS Community Edition and I use Arcane as my Docker manager, but the examples below are intentionally generic so they can be adapted to other Docker setups.
A few things before we start
You will often see something like:
PUID: 1000
PGID: 1000
or: user: "1000:1000"
These control what Linux user/group the container runs as.
You need to change these values to match the user that owns or has permission to access your media directories.
You can normally check your user with:
id
For example:
uid=1000(username) gid=1000(username)
would normally mean:
PUID: 1000
PGID: 1000
for my own setup i use 568 becuase truenas app preset is 568 if you run TN then opt for 568 not 1000
Make sure the containers that need to share/write the same files have compatible permissions.
2. Comments in YAML
Anything after # is a comment.
For example:
TZ: Etc/UTC # Change this to your timezone
Docker Compose ignores everything after the #.
You can use comments to leave yourself notes or temporarily disable lines.
3- you may see in some yaml that I use https://book.mydomain.com you will want to do something similar if you wanna read or listen remotely the way I do it in my server I run cloudflare tunnel and nginx reverse proxy and you need to buy a cheap domain like 1$ a year (there are a lot of tutorial in doing something like that and its out the scope of this guide)
4- you will see in some example (# use for POSTGRES_PASSWORD # run openssl rand -hex 24) you would wanna run that in terminal or shell to generate random passphrase then copy that and paste it where its needed
For example
openssl rand -hex 24
openssl rand -hex 32
HERE WE GO :) :) :)
BookOrbit
-----------------------------------------------------------------------
services:
bookorbit:
image: ghcr.io/bookorbit/bookorbit:latest
container_name: bookorbit-app
restart: unless-stopped
init: true
ports:
- "3000:3000"
environment:
NODE_ENV: production
PORT: 3000
# Public URL through your reverse proxy.
# Replace with your own domain.
APP_URL: https://books.example.com
CLIENT_URL: https://books.example.com
# PostgreSQL
POSTGRES_HOST: bookorbit-db
POSTGRES_PORT: 5432
POSTGRES_USER: bookorbit
# Generate with run this in shell/terminal:
# openssl rand -hex 24
POSTGRES_PASSWORD: CHANGE_ME_POSTGRES_PASSWORD
POSTGRES_DB: bookorbit
# Security
# Generate with run this in shell/terminal:
# openssl rand -hex 32
JWT_SECRET: CHANGE_ME_JWT_SECRET
# Generate with run this in shell/terminal:
# openssl rand -hex 16
SETUP_BOOTSTRAP_TOKEN: CHANGE_ME_BOOTSTRAP_TOKEN
# Change these to match your media permissions
PUID: 1000
PGID: 1000
# Change to your timezone
TZ: Etc/UTC
# Start BookOrbit's library browser here
LIBRARY_BROWSE_ROOT: /books
# Automated ingest staging area.
# Drop files here for BookOrbit to process them.
BOOK_DOCK_PATH: /data/book-dock
# Container-aware memory sizing
NODE_MAX_OLD_SPACE_SIZE: auto
# Encrypt stored SMTP credentials if configured
# EMAIL_ENCRYPTION_KEY:
# Keep normal username/password login enabled
DISABLE_LOCAL_AUTH: "false"
depends_on:
bookorbit-db:
condition: service_healthy
volumes:
# BookOrbit-owned books and audiobooks
- /mnt/path/to/books/bookorbit:/books
# BookOrbit application state
- /mnt/path/to/bookorbit/app:/data
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
cap_add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
security_opt:
- no-new-privileges:true
stop_grace_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
bookorbit-db:
image: pgvector/pgvector:pg18
container_name: bookorbit-db
restart: unless-stopped
environment:
POSTGRES_USER: bookorbit
# MUST match POSTGRES_PASSWORD above
POSTGRES_PASSWORD: CHANGE_ME_POSTGRES_PASSWORD
POSTGRES_DB: bookorbit
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- /mnt/path/to/bookorbit/postgres:/var/lib/postgresql/data
healthcheck:
test:
- CMD-SHELL
- 'pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"'
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
BookBridge
Change 1000:1000 to the UID/GID used by your media files.
The BookOrbit library is mounted as read-only using :ro.
services:
bookbridge:
image: ghcr.io/cporcellijr/bookbridge:latest
container_name: bookbridge
user: "1000:1000"
ports:
- "5757:5757"
environment:
TZ: Etc/UTC
LOG_LEVEL: INFO
# Make runtime caches writable by the container user
HOME: /data
XDG_CACHE_HOME: /data/.cache
HF_HOME: /data/huggingface
volumes:
# BookBridge application data/cache
- /mnt/path/to/bookbridge/data:/data
# Existing BookOrbit library
- /mnt/path/to/books/bookorbit/books:/books:ro
restart: unless-stopped
Storyteller
After deploying Storyteller, you need to create a secret key.
You can generate one with:
openssl rand -base64 32
Then place the generated key inside a text file named:
STORYTELLER_SECRET_KEY.txt
For example:
/mnt/path/to/storyteller/STORYTELLER_SECRET_KEY.txt
The Docker Compose configuration can then expose that file to Storyteller as a Docker secret.
services:
storyteller:
image: registry.gitlab.com/storyteller-platform/storyteller:latest
container_name: storyteller
restart: unless-stopped
ports:
- "8001:8001"
volumes:
# Storyteller application data/database/transcriptions/cache
- /mnt/path/to/storyteller/data:/data:rw
# Writable Next.js cache
- /mnt/path/to/storyteller/next-cache:/app/.next/standalone/applications/web/.next/cache:rw
# Existing BookOrbit library containing EPUB + M4B
- /mnt/path/to/books/bookorbit/books:/library:rw
environment:
- STORYTELLER_SECRET_KEY_FILE=/run/secrets/secret_key
# Change these to match your media permissions
- PUID=1000
- PGID=1000
# Logging level
- STORYTELLER_LOG_LEVEL=info
secrets:
- secret_key
secrets:
secret_key:
file: /mnt/path/to/storyteller/STORYTELLER_SECRET_KEY.txt
Audiobookshelf
Notice that the audiobook directory below ends with:
:ro
That means read-only.
Audiobookshelf can read the audiobook files but cannot modify or delete them through this mount.
I do this because my Audiobookshelf library contains hardlinks to the BookOrbit library and I prefer BookOrbit to be the main owner/manager of those files.
Audiobookshelf can still keep its own database, metadata, covers, progress information, etc. inside /config and /metadata.
-----------------------------------------------------------------------
services:
audiobookshelf:
image: ghcr.io/advplyr/audiobookshelf:latest
container_name: audiobookshelf
ports:
- "13378:80"
environment:
TZ: Etc/UTC
volumes:
# Audiobookshelf configuration
- /mnt/path/to/audiobookshelf/config:/config
# Audiobookshelf metadata
- /mnt/path/to/audiobookshelf/metadata:/metadata
# Audiobooks
# :ro = read-only
- /mnt/path/to/books/audiobookshelf/audiobooks:/audiobooks:ro
# Podcasts
- /mnt/path/to/books/audiobookshelf/podcasts:/podcasts
user: "1000:1000"
restart: unless-stopped
Double Commander
Double Commander needs access to the common parent directory containing both libraries so you can easily work between them.
For example:
/mnt/path/to/books/
├── bookorbit/
└── audiobookshelf/
Compose:
-----------------------------------------------------------------------
services:
doublecommander:
image: lscr.io/linuxserver/doublecommander:latest
container_name: doublecommander
environment:
PUID: "1000"
PGID: "1000"
TZ: Etc/UTC
volumes:
# Double Commander configuration
- /mnt/path/to/doublecommander/config:/config
# Parent directory containing your book libraries
- /mnt/path/to/books:/books
ports:
# Optional additional port depending on your setup:
# - "3000:3000"
- "3002:3001"
shm_size: "1gb"
restart: unless-stopped
How to create a hardlink in Double Commander
Open your BookOrbit directory in one panel.
For example:
/books/bookorbit/
Open your Audiobookshelf directory in the other panel:
/books/audiobookshelf/
Select the EPUB or audiobook you want to link, then choose:
File -> Create Hard Link
You should immediately see the new file appear in the opposite directory.
The two filenames now point to the same underlying file data, so you have not duplicated the audiobook or ebook.
IMPORTANT
Hardlinks can only be created when both locations are on the same filesystem.
If you use ZFS, this generally means they must also be inside the same ZFS dataset.
For example, this can work:
Dataset: books
books/
├── bookorbit/
└── audiobookshelf/
But if bookorbit and audiobookshelf are two separate ZFS datasets/filesystems, you cannot hardlink a file between them.
Second Part: Client-side
1. Kindle / Kobo + KOReader
KOReader is an amazing open-source reading application.
For Kindle, you generally need to jailbreak the device before installing KOReader.
This is an amazing community resource that makes the Kindle process much easier:
https://kindlemodding.org/jailbreak-wizard.html
Kobo devices have their own KOReader installation methods https://github.com/koreader/koreader/wiki/Installation-on-Kobo-devices. i recommend the manual method https://github-wiki-see.page/m/koreader/koreader/wiki/Installation-on-Kobo-devices
Once KOReader is installed, you can also install/configure the BookOrbit integration and access your self-hosted library from the e-reader.
2. iOS clients
The main apps I use are:
BookOrbit
Storyteller
Prologue
Install them on your iPhone/iPad and connect them to the corresponding services on your server.
My general setup is:
BookOrbit App
|
+--> BookOrbit server
|
+--> EPUB / Audiobook / Read-Aloud
Storyteller App
|
+--> Storyteller server
|
+--> Read-Aloud books
Prologue
|
+--> Audiobookshelf
|
+--> Audiobooks
3. Read-Aloud
Both the BookOrbit app and Storyteller app support the Read-Aloud workflow.
I personally prefer the Storyteller app for this.
4. Apple Watch
I’ll add more details about my Apple Watch setup here later as I continue updating this guide.
That’s the stack!
The idea is basically:
BookOrbit = Main book library
BookBridge = Progress synchronization between formats
Storyteller = EPUB + audiobook -> Read-Aloud book
Audiobookshelf = Dedicated audiobook tools/clients
DoubleCommander = Easy hardlink management
KOReader = E-reader client
BookOrbit App = General mobile reading/listening
Storyteller = Read-Aloud client
Prologue = Audiobook client
There are obviously many different ways to build a self-hosted reading setup, but this is the workflow that works for me.
I’ll keep updating the guide as I improve my own stack or find better ways of doing things.
--------------------------------------------------------------------------------------------
if you use NGINX ABS require also "Custom Nginx Configuration"
ABS:
-----------
client_max_body_size 10240M;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_redirect http:// https://;
---------