If your ebooks live in a Calibre folder on one laptop and your comics sit in a pile of CBZ files you can only open at a desk, you already know the problem: your reading library is stuck to one machine. Meanwhile the commercial ebook stores lend you licenses that can be revoked, sync your highlights to their servers, and lock your purchases to their apps. Kavita is the fix. It is a fast, self-hosted reading server that turns a folder of EPUBs, PDFs, CBZ comics, and manga into a polished web reader with a proper library, reading progress that follows you across devices, and support for standard mobile reading apps through OPDS.
This guide walks through a production-ready Kavita install on a VPS using Docker, with Caddy in front for automatic HTTPS. It covers the parts that actually trip people up: how to lay out your folders so Kavita scans them cleanly, how OPDS unlocks the reading apps you already use, and how to add family members without handing them the keys to the whole server.
books.example.com at your serverdocker-compose.ymlconfig (database and settings) and library (your books, read-only)Total time: about 15 minutes.
80 and 443 open to the internet (Let's Encrypt needs them)Kavita is one of the lighter media servers you can run. The database is SQLite, serving a page is cheap, and the only mildly heavy operations are the initial scan and cover-image extraction. Disk for the books themselves is the real sizing question - budget accordingly if you keep high-resolution scans.
In your DNS provider, create an A record:
books.example.com → YOUR_VPS_IPV4
Add an AAAA record too if your server has IPv6. Confirm it resolves:
dig +short books.example.com
The output should be your VPS IP. Caddy cannot issue a certificate until DNS points at the box.
On a fresh Ubuntu 22.04 or 24.04 server:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Check it works:
docker --version
docker compose version
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Caddy needs 80 for the ACME HTTP challenge and 443 for HTTPS. Do not expose Kavita's internal port 5000 to the internet - Caddy is the only thing that should answer publicly.
sudo mkdir -p /opt/kavita
cd /opt/kavita
sudo mkdir -p config library caddy-data caddy-config
The two data directories map to how Kavita splits state:
config/ holds the SQLite database, settings, extracted cover images, cache, and logslibrary/ is where your books live, mounted read-onlyIf your VPS has a large data disk mounted elsewhere, point the library folder at it:
sudo mkdir -p /mnt/storage/books
sudo ln -s /mnt/storage/books /opt/kavita/library
Kavita builds its library from folder and file names, so a consistent layout is the single most important thing you do. Kavita groups content by library type, and each type expects a slightly different structure. The reliable pattern is one folder per series, with volumes and chapters encoded in the file names.
For a manga or comic library:
library/
comics/
Saga/
Saga Vol. 1.cbz
Saga Vol. 2.cbz
Invincible/
Invincible #001.cbz
Invincible #002.cbz
For an ebook library:
library/
books/
Andy Weir/
Project Hail Mary.epub
Ursula K. Le Guin/
The Dispossessed.epub
Kavita understands Vol., Volume, Ch., Chapter, and issue-number patterns like #001. EPUB files carry their own metadata, so Kavita reads title, author, and series straight from the file - naming matters most for comics and manga where the container format has no rich metadata. Keep one series per folder and you avoid almost every scan headache.
Kavita reads EPUB, PDF, CBZ, CBR, CB7, CBT, and plain images. It never modifies your files, which is exactly why the library volume is mounted read-only later.
Create /opt/kavita/docker-compose.yml:
services:
kavita:
image: jvmilazz0/kavita:latest
container_name: kavita
restart: unless-stopped
environment:
TZ: "Europe/Berlin"
volumes:
- ./config:/kavita/config
- ./library:/library:ro
networks:
- booknet
caddy:
image: caddy:2
container_name: kavita-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
networks:
- booknet
networks:
booknet:
Notes:
library volume is mounted :ro (read-only) on purpose. Kavita never needs to write to your books, and a read-only mount means a bug or a bad actor cannot delete your collection./library/comics, /library/books, and so on - those are the paths inside the container.5000 stays on the booknet Docker network. Caddy reaches it by container name, so it never touches the public internet directly.TZ to your zone so reading history timestamps and scheduled scans line up with your clock.Create /opt/kavita/Caddyfile:
books.example.com {
encode zstd gzip
reverse_proxy kavita:5000 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up Host {host}
}
}
The X-Forwarded-Proto header is the one that matters most. Without it Kavita can build http:// links internally, which breaks OPDS clients and the invite links you send to new users. Caddy handles the certificate, renewal, and HTTP-to-HTTPS redirect with no extra config.
cd /opt/kavita
sudo docker compose up -d
sudo docker compose logs -f
Wait until the Caddy logs show the Let's Encrypt certificate was issued, then open https://books.example.com. You should land on the Kavita first-run setup screen.
The first screen asks you to register the admin user. This is the all-powerful account, so pick a strong password and a real email address - Kavita uses email for password resets and for inviting other users. Once you are in, you land on an empty dashboard ready for its first library.
https://books.example.com. If it still shows an internal address or plain http, invite links and OPDS URLs will point somewhere unreachable. This is the number-one cause of "the app cannot connect" reports.
In the admin dashboard, go to Settings -> Libraries -> Add Library. For each one:
Comics, Books, Manga).Manga reads right-to-left, Comic left-to-right, Book uses the EPUB reader./library/comics or /library/books.Save it and Kavita immediately starts scanning in the background. You can watch progress in the logs:
sudo docker compose logs -f kavita
A few hundred books index in under a minute. Large comic collections take longer because Kavita extracts a cover image from every file. Let the first scan finish before you decide whether anything is missing.
Kavita has real multi-user support with per-user reading progress, bookmarks, and want-to-read lists. Invite people under Settings -> Users -> Invite:
Books but not the rest.Each account keeps its own reading position, so your progress through a series never collides with anyone else's.
This is where a self-hosted library pays off. Kavita speaks OPDS, the open catalog standard supported by a wide family of reading apps, so you are not locked into one official client. Enable it first under Settings -> General -> Enable OPDS, then grab your personal feed URL from your user account page (Settings -> Account -> 3rd Party Clients / OPDS URL). It looks like:
https://books.example.com/api/opds/YOUR-API-KEY
Paste that URL into an OPDS-capable reader:
The API key in the URL is effectively a password, so treat it like one. If it leaks, regenerate it from your account page and every old link stops working immediately.
Comics and manga carry little embedded metadata, so Kavita occasionally guesses a series name or grabs an ugly cover. You can correct it without touching the files:
ComicInfo.xml file inside a CBZ - Kavita reads it for accurate series, volume, and writer metadata.Locking a field is the trick most people miss. Once locked, your manual title survives every rescan.
Your book files are presumably backed up already. The piece unique to Kavita - user accounts, reading progress, bookmarks, collections, and metadata edits - lives in the config directory. Losing it means every family member starts their series over from page one. Pair it with a nightly restic job to object storage:
restic -r s3:s3.amazonaws.com/my-backup-bucket backup /opt/kavita/config
Kavita also writes its own periodic database backups into config/backups/, which the restic job sweeps up automatically. The directory is small - usually well under a gigabyte - so backups are fast and cheap. Restoring is just dropping the folder back and starting the container.
If the only people using your server live in your house, it does not need to face the public internet at all. Putting Kavita behind Tailscale removes every bot scan and login attempt in one move. Skip the firewall openings for 80 and 443, reach books.example.com over your tailnet, and OPDS apps connect exactly the same way. A WireGuard VPN on your VPS works just as well if you already run one.
Caddy returns a 502 right after docker compose up. Kavita takes a few seconds to build its database on first boot, and Caddy can hit it first. Wait 30 seconds and reload. If it sticks, check docker compose logs kavita.
The library is empty after a scan. Almost always a path or permission issue. Confirm the folder you added in the UI matches a real path under /library, and that the container can read it: docker exec -it kavita ls -la /library. If you see permission denied, fix ownership on the host with sudo chown -R 1000:1000 /opt/kavita/library.
Series are split, merged, or named wrong. This is a naming problem, not a Kavita bug. Keep one series per folder, use consistent Vol. and #001 patterns, and re-run the scan. For comics, a ComicInfo.xml inside the CBZ removes the guesswork entirely.
An OPDS app says it cannot connect with the right URL. Confirm OPDS is enabled in settings and that the server base URL is the public https:// hostname, not an internal address. Copy the OPDS URL fresh from your account page - a regenerated API key invalidates every old link.
Reading progress is not syncing between devices. Progress is tied to your user account, so make sure both devices log in as the same user. In OPDS apps that only download files, progress lives in the app, not on the server - use the Kavita web reader or an app that reports progress back for cross-device sync.
ComicInfo.xml. Kavita then shows accurate writers, publishers, and release dates with zero manual editing.config folder into a nightly restic job so a dead disk never costs you your accounts and reading history.That's it. Self-hosted Kavita gives you a fast, private reading library built from books and comics you actually own, reachable from any device, with no subscription, no revoked licenses, and no reading data handed to a store.
Need a VPS for your reading library? Our Linux plans ship with NVMe storage, generous bandwidth, and a storage tier for big comic and manga collections. See the options.