Skip to content
Infrastructure

Consolidating audiobooks and ebooks into a single Audiobookshelf

By Victor Da Luz
homelab audiobookshelf proxmox zfs ansible

I was running two separate services for my personal library: Audiobookshelf for audiobooks and Kavita for ebooks. I had set them up months earlier as two small projects (audiobooks here, ebooks here), and they both worked fine. The problem was that they were two of everything for one job.

Audiobookshelf added ebook support a while back. It reads EPUB, PDF, CBZ, and CBR, and it has a built-in web reader. ShelfPlayer, my iOS client, handles the audiobooks, and the ebooks I read in the Audiobookshelf web reader in a browser. So Kavita wasn’t giving me anything Audiobookshelf couldn’t. When I rebuilt the homelab as v3, consolidating made sense.

This is the story of replacing both services with one Audiobookshelf instance, the Ansible role I wrote to deploy it, and a storage decision that went sideways in a very homelab way.

The setup I was replacing

In v2 I had two LXC containers: one ran Audiobookshelf with an SMB mount to the NAS for audiobooks, the other ran Kavita with an SMB mount to the same NAS for ebooks. Both were privileged containers running Docker, both mounted the same NAS media share, just different subdirectories.

Two containers, two Ansible roles, two Traefik routes, two things to keep patched. For one library.

Storage: local instead of NAS

The biggest change from v2 is where the media lives. In v2 both services mounted the NAS for their libraries. That works, but it makes the NAS a hard dependency: if it’s off or unreachable, the service is useless. In v3 I kept the media local and made the NAS a backup target instead.

The container gets a dedicated 300GB disk. Proxmox makes this easy: you add a mount point to the LXC config pointing at a ZFS subvolume, and it shows up at /media inside the container on every boot.

pct set <vmid> -mp0 media-pool:300,mp=/media

The NAS is still mounted, just at /mnt/nas instead of /media. It’s there for backups and manual transfers, but Audiobookshelf doesn’t read from it. If the NAS goes offline, the service keeps working.

The Ansible role

The role is short. It checks Docker is installed, creates the service directories and the library folders at /media/audiobooks and /media/ebooks, installs the docker-compose manifest, and verifies the health endpoint after startup. About 80 lines of tasks.

The compose is minimal on purpose. Audiobookshelf doesn’t need a separate database or cache container, it handles all of that internally. One container, three volume mounts:

volumes:
  - /opt/audiobookshelf/data/config:/config
  - /opt/audiobookshelf/data/metadata:/metadata
  - /media:/media

NAS credentials are optional parameters. If you don’t pass them, the role skips the NAS mount tasks, which is handy for deploying somewhere without NAS access or for standing up the local setup first.

The USB pool incident

The 300GB disk lives on a ZFS pool backed by a USB-attached ADATA SX 6000LNP SSD. I was reorganizing the rack while a 196GB rsync was running, moving cables and shifting things around, and I knocked the USB connection loose.

ZFS suspended the pool immediately. That is the correct behavior: it won’t keep writing to a disconnected device and risk silent corruption. The rsync died, the container’s /media went inaccessible, and zpool status showed a suspended pool with 1,155 checksum errors.

The fix, once the drive was plugged back in:

zpool clear media-pool

The pool came back online with no data loss. The files partially written during the disconnect were the ones flagged with checksum errors, but since the source was all still on the NAS, I re-ran the rsync with --checksum to verify and overwrite anything corrupt:

rsync -a --checksum /mnt/nas/audiobooks/ /media/audiobooks/

The --checksum flag is slower, it reads and hashes every file on both sides instead of trusting modification time and size, but for a recovery it’s the right call. You want to know exactly what landed correctly, not what rsync assumes changed.

Traefik routing

Adding Audiobookshelf to Traefik was one entry in the dynamic config template, which reads from my services.yaml state file. I added the service there, then the template generated the router and backend:

audiobookshelf:
  rule: "Host(`audiobookshelf.example.net`)"
  service: audiobookshelf

One redeploy of the Traefik playbook and the route was live. The wildcard cert (*.example.net) already covered it, so no certificate work.

Migration

The migration was just rsync from the NAS to the local disk. Both the old audiobook and old ebook libraries lived on the same NAS share in separate subdirectories, so it was two copies running in parallel inside the container:

rsync -a /mnt/nas/audiobooks/ /media/audiobooks/
rsync -a /mnt/nas/ebooks/ /media/ebooks/

Ebooks finished in minutes (1.5GB). Audiobooks took a few hours (196GB). Once the copy was done I opened the Audiobookshelf web UI, added both library folders, and it scanned and matched metadata automatically. ShelfPlayer picked up the audiobooks the next time it synced, and the ebooks were there in the web reader.

Result

One service instead of two. Same libraries, one URL, one container to maintain, one Ansible role to update. Kavita got torn down separately.

The storage setup is also cleaner than v2. The libraries live locally so the service isn’t NAS-dependent, but the NAS is still mounted for backups when I want it. Proxmox Backup Server handles container-level backups nightly, so config and metadata are covered. The media itself I back up with a scheduled rsync from /media to the NAS.

A few things I took from it:

  • Consolidating only made sense because Audiobookshelf grew into the ebook job. One server reading both, with a web reader for ebooks and ShelfPlayer for audiobooks, beat running Kavita alongside it.
  • Local media with the NAS as backup is more resilient than mounting the NAS as the library. The service survives the NAS going away.
  • ZFS suspending a pool on a USB disconnect is a feature, not a failure. zpool clear plus a --checksum rsync from a known-good source got me back with no data loss.

The two posts this replaces are where it started: the audiobook setup and the Kavita ebook setup. v3 is where they became one.

Disclosure: As an Amazon Associate, I earn from qualifying purchases.

Related reading

Infrastructure

Self-hosting my audiobooks with Audiobookshelf

I wanted my Audible library streamed to my phone, position-synced, and not locked inside one app. Audiobookshelf got a container in the homelab. Here is the deploy, the WebSocket gotcha, the iOS client I picked, and the work of getting an Audible library out of DRM.

Read

Ready to Transform Your Career?

Let's work together to unlock your potential and achieve your professional goals.