Self-Hosting an Ebook Library So I Don't Rent My Books
The last media I was still renting
Right after I brought my music home, books moved to the top of the list. They’d been nagging at me for a while: of everything I read, watch, and listen to, my reading was the one corner still scattered across accounts I didn’t control.
My photos, documents, music, and video all live in tools I chose now, but my books were spread across a Kindle account, Apple Books, and a Kobo that wanted me to sign in. An ebook library is exactly the kind of owned default this whole project is about, so it was an easy next service to earn its spot on the laptop.
What this solves
- Replaces Kindle, Apple Books, and Kobo sync with a library I own.
- Keeps every book as a file on my hardware, not a license on someone’s server.
- Lets me read on a Kobo without a vendor account.
- Adds one service that fits the existing storage layout with no special handling.
Why books were the worst offender
Of all my media, books had the most strings attached. Amazon can revoke titles from a Kindle you already paid for. Apple ties purchases to an Apple ID. Even Kobo, the most open of the three, routes sync through a Kobo account and quietly logs what and when I read. None of that is catastrophic on its own. It just runs against the whole point of this project, which is that the path of least resistance should lead to things I actually control.
The honest motivation is small and a little stubborn. I want a book I “bought” to behave like a book I own. That means a file, on a drive, in my house, that no terms-of-service update can reach.
Two tools sharing one library
The design is the part worth understanding, because it’s simpler than it looks. The stack is two tools that point at the same folder: Calibre and Calibre-Web. Calibre is the gold-standard open source ebook manager, and I run it headless, driving it entirely through its calibredb command-line tool. Calibre-Web is a small Flask web app that gives me a clean browser interface for the same library.
The key detail: they share one library folder and one metadata.db. There is no sync step between them. A book I add from the command line shows up in the browser on the next page load, and a book I drag into the browser is visible to calibredb immediately. One source of truth, two ways in. That’s what keeps this calm instead of becoming a two-system juggling act.
Fitting it into the /srv split
The raw setup guides all drop the library in your home directory at ~/calibre-library. That’s fine on a desktop, but it fights the layout I committed to back in part 5: app config under /srv/appdata/, big user data under /srv/storage/. Part 11 was basically me wishing I’d locked that split in on day one, so I wasn’t about to break it for a new service.
So the books live at /srv/storage/books, treated like any other media library, and Calibre-Web’s own config goes in /srv/appdata/calibre-web/. That one change means backups and reasoning about the server stay consistent. The library is media, the config is config, and nothing lands in a home directory I’d forget to protect.
Installing it without the GUI
Calibre installs headless with the project’s official one-liner, which ships a newer version than Debian’s repo and pulls in its own dependencies:
sudo -v && wget -nv -O- https://download.calibre-ebook.com/linux-installer.sh | sudo sh /dev/stdin
That puts calibredb and friends on the PATH. Pointing it at an empty folder once creates the database, and migrating my existing desktop library over was a single rsync of the old “Calibre Library” folder into /srv/storage/books, metadata and covers intact.
Calibre-Web runs as its own unprivileged user in a Python virtualenv, managed by systemd so it comes back after a reboot. The unit is small:
[Unit]
Description=Calibre-Web
After=network.target
[Service]
Type=simple
User=calibreweb
ExecStart=/opt/calibre-web/venv/bin/cps -p /srv/appdata/calibre-web
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
First login is the default admin / admin123, which I changed before anything else, then I pointed it at /srv/storage/books and the migrated library appeared. The dedicated calibreweb user just needs read and write access to that folder.
The interesting part: a Kobo with no account
Here’s the bit I actually find satisfying. The Kobo never signs into anything. I plug it into the server over USB, copy books onto it, eject, and unplug. No Kobo account, no over-the-air sync, no telemetry. It’s the oldest, dumbest workflow in the world, and it’s exactly what ownership looks like.
To make it painless, a udev rule mounts the Kobo automatically the moment it’s plugged in. udev is Linux’s device manager, and it can run a script when it sees a specific device by its USB vendor and product ID (find yours with lsusb):
ACTION=="add", ATTRS{idVendor}=="2237", ATTRS{idProduct}=="4b80", RUN+="/home/youruser/.local/bin/mount-kobo.sh"
ACTION=="remove", ATTRS{idVendor}=="2237", ATTRS{idProduct}=="4b80", RUN+="/home/youruser/.local/bin/umount-kobo.sh"
One catch worth flagging: udev runs as root, so the ~ shortcut doesn’t expand. The paths in that rule have to be absolute or the script silently never fires. That cost me a confused ten minutes.
Sending a book is then one command. A small kobo-send script wraps calibredb export with Kobo-friendly defaults, skipping the cover and OPF sidecar files the Kobo doesn’t use and flattening everything into one directory:
calibredb export "$@" \
--to-dir /mnt/kobo \
--dont-save-cover --dont-write-opf --single-dir \
--library-path /srv/storage/books
The daily rhythm is four steps. Plug in the Kobo, run kobo-list to find a book’s ID, kobo-send 4 to copy it, and eject-kobo before unplugging. That eject step matters more than it sounds, because it runs sync first to flush pending writes and avoid corrupting the Kobo’s filesystem. A handful of shell aliases turn the whole thing into muscle memory.
What it costs to keep
Every service has to earn its ongoing cost, and this one is cheap. Because the library lives under /srv/storage, it needs no special backup handling; whatever protects that tree protects the books too, with no extra config. There’s no database to dump carefully like Immich or Paperless, just a folder of files and a metadata.db, which is about the friendliest thing you can ask a backup to protect.
Reaching it is tidy too. Instead of typing an IP and port, I gave it a name through the Pi-hole and Caddy setup from part 10, so the library opens at a clean local hostname over HTTPS. And since it’s LAN-only, there’s nothing exposed to the internet. If I ever wanted to read from outside the house, Tailscale would handle that without opening a single port.
What’s next
This is the one I’m most quietly pleased with. Not because it’s clever, it isn’t, but because it closes the last gap between “media I rent” and “media I own.” A folder of files, a browser to browse them, and a Kobo I fill over a cable like it’s 2008. That’s the calm version of a personal library.
That’s books home, alongside the photos, documents, music, and video. There’s no grand finish line here, just one fewer thing I rent, and probably one more service down the road when it earns the spot. For the full map of how it fits together so far, the series index collects every part in order.