Approach Comparison: Dedicated Audiobook Servers vs. General Media Stacks
Picture this: you are 12 hours into a 30-hour fantasy epic. You hop in the car, open your audio player, and discover your progress reset to minute zero. If you have ever tried shoehorning an audiobook library into Plex or Jellyfin, you have likely run into this exact headache.
General media servers treat audiobooks like standard music tracks. Switch from your desktop browser to your phone on the subway, and you run into dropped timestamps, missing chapter splits, and shuffled multi-disc files. Homelabers typically handle spoken-word audio through one of three setups:
- The Repurposed Music Server (Navidrome / Jellyfin): Great for FLAC and MP3 music albums. However, they lack native
.m4bchapter parsing, pitch-corrected variable speeds (like 1.25x or 1.75x), and granular bookmarks. Resuming a file over a spotty 4G connection frequently fails. - The Plex + Third-Party Companion Stack (Plex + Prologue/Chronicle): Works smoothly on iOS with Prologue. Still, it depends on fragile third-party scrapers and strict file-naming rules. Android users are left out in the cold without equivalent dedicated clients.
- The Dedicated Spoken-Word Server (Audiobookshelf): Built from the ground up for audiobooks and podcasts. It couples local SQLite caching with real-time WebSocket syncing. It also pulls metadata directly from Audible, Google Books, and OpenLibrary, backed by polished apps for both Android and iOS.
Pros & Cons of Running Audiobookshelf on Docker
Audiobookshelf runs lean—idling at roughly 120MB to 200MB of RAM. Still, it has operational quirks you should know before pulling the image.
Pros
- Sub-Second Progress Sync: Persistent WebSocket connections track playback down to the second across the web player, Android, and iOS.
- Automatic Chapter Extraction: The parser reads embedded cue points and chapter titles straight from
.m4b,.mp3, and.flacfiles without external sidecar files. - Isolated Metadata Storage: Author bios, series sequences, and high-res covers live in a local cache. Your bulk audio library remains completely untouched.
- Built-In Podcast Downloader: An integrated RSS parser downloads new episodes on a schedule, replacing the need for separate tools like TubeArchivist or Podurama.
Cons
- Permission Strictness: The container process enforces matching User IDs (PUID) and Group IDs (PGID). If host permissions drift, SQLite will lock with an
SQLITE_BUSYerror or fail silently during scheduled library scans. - Proxy WebSocket Requirements: Because real-time syncing relies on continuous duplex connections, reverse proxies like Nginx, Traefik, or Cloudflare must pass explicit upgrade headers. Miss one header, and mobile sessions fall back to broken polling loops.
Recommended Setup Architecture
Never place your database files directly on a network share. To avoid file locks and slow scan times, split your configuration from your media files.
Store your configuration and SQLite databases on a fast local SSD (such as /opt/audiobookshelf). Keep bulk audio files on your mechanical hard drive array or ZFS pool, mounted either locally or through NFS.
/opt/audiobookshelf/ # Local NVMe/SATA SSD
├── config/ # App settings, users, and session tokens
└── metadata/ # SQLite database, cached covers, stream cache
/mnt/storage/media/ # Bulk storage (ZFS, Unraid array, or mergerfs)
├── audiobooks/ # Author/Series/Book Name/Book.m4b
└── podcasts/ # Show Name/Episode - Title.mp3
This layout prevents headaches. If your spinning disks sleep after 20 minutes of inactivity or your ZFS pool runs a heavy monthly scrub, the web interface stays fast. The SQLite database remains snappy because all I/O queries execute against local flash memory.
Implementation Guide
Here is how to set up the host directory tree, write the Compose specification, set up WebSocket proxy routing, and verify playback sync across devices.
Step 1: Prepare Host Directories and UID/GID Permissions
Run these commands on your Docker host to create the required paths and set ownership. Running rootless or matching your non-root system user avoids permission deadlocks on persistent mounts.
# Check your current user ID and group ID
id
# Typical output: uid=1000(ubuntu) gid=1000(ubuntu)
# Create runtime, config, and media directories
sudo mkdir -p /opt/audiobookshelf/config /opt/audiobookshelf/metadata
sudo mkdir -p /mnt/storage/media/audiobooks /mnt/storage/media/podcasts
# Assign ownership to your unprivileged user (UID/GID 1000)
sudo chown -R 1000:1000 /opt/audiobookshelf
sudo chmod -R 755 /opt/audiobookshelf
Step 2: Define the Docker Compose Manifest
Create a dedicated directory for your stack and open a new compose file:
mkdir -p ~/stacks/audiobookshelf
cd ~/stacks/audiobookshelf
nano docker-compose.yml
Paste the following production configuration:
services:
audiobookshelf:
image: ghcr.io/advplyr/audiobookshelf:latest
container_name: audiobookshelf
restart: unless-stopped
environment:
- AUDIOBOOKSHELF_UID=1000
- AUDIOBOOKSHELF_GID=1000
- TZ=Etc/UTC
- PORT=13378
ports:
- "13378:13378"
volumes:
- /opt/audiobookshelf/config:/config
- /opt/audiobookshelf/metadata:/metadata
- /mnt/storage/media/audiobooks:/audiobooks:ro
- /mnt/storage/media/podcasts:/podcasts
healthcheck:
test: ["CMD", "nc", "-z", "127.0.0.1", "13378"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
Security Tip: Mounting /mnt/storage/media/audiobooks with the :ro read-only flag prevents the container from accidentally writing to or deleting original files. Remove :ro only if you want Audiobookshelf to edit embedded ID3 tags in place.
Step 3: Deploy and Inspect Runtime Logs
Start the container in detached mode and inspect the initial startup sequence:
docker compose up -d
docker compose logs -f audiobookshelf
Confirm that the database initialized and the socket listener bound cleanly:
[INFO] Init db: SQLite connected
[INFO] Server listening on port 13378
[INFO] Websocket server initialized
If you spot EACCES or SQLITE_BUSY warnings, double-check that UID 1000 owns /opt/audiobookshelf/config and /opt/audiobookshelf/metadata using ls -la /opt/audiobookshelf.
Step 4: Configure Reverse Proxy with WebSocket Headers
Exposing Audiobookshelf over HTTPS through Nginx requires explicit WebSocket handling. Without Upgrade headers, mobile clients drop connections and fail to push listening timestamps.
server {
listen 443 ssl http2;
server_name audio.homelab.local;
# SSL certificate paths
ssl_certificate /etc/letsencrypt/live/audio.homelab.local/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/audio.homelab.local/privkey.pem;
location / {
proxy_pass http://127.0.0.1:13378;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Essential WebSocket upgrade headers
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}
Step 5: Library Setup and Client Synchronization Verification
- Open your browser and navigate to
http://<HOST-IP>:13378. - Create your primary root administrator account and set a strong passphrase.
- Select Add Library, pick Audiobooks, and point the path to
/audiobooks. Select the Author/Title directory pattern to speed up parsing. - Download the Audiobookshelf client from Google Play, F-Droid, or Apple TestFlight / App Store.
- Log in to your server instance on your mobile device, start playing a title, and scrub to timestamp
00:18:45. - Open the same title on your desktop browser. Within 1 to 2 seconds, the web player should update automatically to match the mobile playback head.
Step 6: Automated Backups
Protect your listening stats and bookmarks. Go to Settings > Backups and enable daily automated backups at 03:00 AM. Audiobookshelf packages your SQLite state, user sessions, and custom bookmarks into compact .tar archives inside /opt/audiobookshelf/metadata/backups. Sync this folder off-site with Restic or Rclone to ensure complete recoverability.

