Kavita is a self-hosted reading server for manga, comics, webtoons, and ebooks. It reads CBZ, CBR, CB7, ZIP, RAR, EPUB, and PDF directly, serves them through a fast web reader that works on phones and tablets, tracks reading progress per user, and exposes an OPDS feed so external reader apps can browse and download from your library. It is built on .NET and is noticeably lighter on memory than JVM-based alternatives.
This guide deploys Kavita on a single RamNode VPS with Docker Compose, the container bound to localhost, Nginx terminating TLS, and the library mounted read-only into the container.
Architecture Overview
- Kavita container. A .NET application serving the web UI, the REST API, the OPDS feed, and a SignalR hub that pushes scan progress and library updates to open browser sessions.
/kavita/config. Holds the SQLite database,appsettings.json, cached cover images, logs, and Kavita's own scheduled backups. This path is fixed. The image expects it exactly there.- Library mounts. One host directory per content type. This guide uses
/srv/media/manga,/srv/media/comics, and/srv/media/books. - Nginx. Handles TLS and proxies HTTP and WebSocket traffic to
127.0.0.1:5000.
Kavita never needs to write to your library files. It stores covers and metadata in its own config directory. That lets you mount the library read-only, which protects your files from any bug in a scan or a cleanup task.
What You Will Need
- A RamNode VPS running Ubuntu 24.04 LTS. 1 vCPU and 2 GB RAM handles a large personal library. Kavita idles at a few hundred MB. Library scans and cover generation are the CPU-heavy moments, and they only run on change or on schedule.
- Disk sized for your library. Manga volumes are typically 50 to 200 MB each as CBZ. If your plan supports additional block storage volumes, put media there.
- Root or sudo access.
- A domain or subdomain with an A record pointing at the VPS. This guide uses
kavita.example.com.
Step 1: Prepare the System
apt update && apt upgrade -y
apt install -y ca-certificates curl gnupg ufw rsync
timedatectl set-timezone America/New_YorkAdd swap on 2 GB plans. The first scan of a large library generates thousands of cover thumbnails and benefits from the headroom.
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstabOptional: Mount a Block Storage Volume for Media
lsblk
mkfs.ext4 /dev/vdb
mkdir -p /srv/media
echo "UUID=$(blkid -s UUID -o value /dev/vdb) /srv/media ext4 defaults,nofail 0 2" >> /etc/fstab
mount -a
df -h /srv/mediaKeep /opt/kavita/config on the root disk. The SQLite database should sit on local storage, never a network share.
Step 2: Install Docker
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
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" \
> /etc/apt/sources.list.d/docker.list
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now dockerCap container log size.
cat > /etc/docker/daemon.json <<'EOF'
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
EOF
systemctl restart dockerStep 3: Create the Directories
mkdir -p /opt/kavita/config
mkdir -p /srv/media/{manga,comics,books}Because the library is mounted read-only, the container only needs read access to it. World-readable directories are enough.
chmod -R a+rX /srv/mediaStep 4: Write the Compose File
pico /opt/kavita/compose.yamlservices:
kavita:
image: jvmilazz0/kavita:latest
container_name: kavita
environment:
- TZ=America/New_York
ports:
- "127.0.0.1:5000:5000"
volumes:
- /opt/kavita/config:/kavita/config
- /srv/media/manga:/manga:ro
- /srv/media/comics:/comics:ro
- /srv/media/books:/books:ro
restart: unless-stoppedNotes on the choices here:
127.0.0.1:5000:5000. Docker's published ports bypass UFW. Binding to loopback keeps the unencrypted port off the public interface.:roon library mounts. Kavita does not write to library files. Read-only mounts cost nothing and remove a whole class of accidents./kavita/configmust not change. The container path is hard-coded in the image. Change the host side freely.- Pin a version. For production, replace
latestwith a release tag from the Kavita GitHub releases page. Anightlytag also exists. Do not use it on a server other people rely on.
Start it.
cd /opt/kavita
docker compose up -d
docker compose logs -fWait for the startup line showing Kavita listening on port 5000, then exit the log stream and check it locally.
curl -sI http://127.0.0.1:5000 | head -1Step 5: Configure the Firewall
apt install -y nginx certbot python3-certbot-nginx
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable
ufw statusStep 6: Configure the Nginx Reverse Proxy
pico /etc/nginx/sites-available/kavitamap $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name kavita.example.com;
client_max_body_size 100M;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
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;
# SignalR hub for live scan progress and library updates.
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 300s;
}
}Enable it and issue a certificate.
ln -s /etc/nginx/sites-available/kavita /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
certbot --nginx -d kavita.example.com --redirect -m you@example.com --agree-tos --no-eff-email
systemctl list-timers | grep certbotServing Kavita Under a Subpath Instead
A dedicated subdomain is the simplest setup. If you need example.com/kavita instead, set the Base URL in Server Settings > General to /kavita/, restart the container, and change the Nginx location to location /kavita/. Kavita stores the base URL in /opt/kavita/config/appsettings.json. Edit it there if you lock yourself out of the UI after a wrong value.
Step 7: First-Run Setup
Browse to https://kavita.example.com. The first visit creates the admin account. Use a strong password and a real email address.
Create libraries under Server Settings > Libraries > Add Library:
- Name it and choose the type. Manga and Comic use the image reader. Book uses the EPUB reader and treats each file as a standalone volume. Pick the type that matches the content, because it changes how Kavita parses filenames.
- Add the folder by its container path:
/manga,/comics, or/books. - Save. Kavita starts the first scan immediately.
Create regular users for daily reading under Server Settings > Users and grant each one only the libraries they should see. Age rating restrictions are available per user if anyone on the server is a child.
Step 8: Load Your Library
Kavita requires a series folder layer. Files dropped directly into a library root will not scan correctly.
/srv/media/manga/
├── Berserk/
│ ├── Berserk v01.cbz
│ ├── Berserk v02.cbz
│ └── Berserk v03.cbz
└── One Punch Man/
├── One Punch Man ch 001.cbz
└── One Punch Man ch 002.cbz
/srv/media/books/
└── Brandon Sanderson/
├── Mistborn - The Final Empire.epub
└── Mistborn - The Well of Ascension.epubVolume and chapter numbers come from the filename. v01, Vol. 1, ch 001, and Chapter 1 all parse. If a series has inconsistent naming, Kavita will still group the files by folder, but sort order may be off until you clean the names. Embedded ComicInfo.xml inside a CBZ takes priority over filename parsing when present.
Upload from your workstation with rsync.
rsync -avP --partial ~/Manga/ root@your-vps-ip:/srv/media/manga/Then make the new files readable and let the folder watcher pick them up, or run Scan Library from the library's menu.
chmod -R a+rX /srv/media/mangaStep 9: OPDS and Reader Apps
Every Kavita user has a personal OPDS URL containing their API key. Find it under User Settings > 3rd Party Clients. It looks like this:
https://kavita.example.com/api/opds/<your-api-key>Paste the full URL into any OPDS-capable reader, such as Panels, Chunky, KOReader, or Moon+ Reader. Treat that URL like a password. Anyone holding it can browse and download from that user's libraries. Rotate the key from the same screen if it leaks.
Email Configuration
Kavita uses email for invites, password resets, and send-to-device. Since v0.7.14 SMTP is configured directly in Server Settings > Email, with no separate relay container needed.
RamNode does not permit mail services on its VPS plans and may restrict outbound SMTP ports. Point Kavita at a transactional mail relay rather than a local MTA, and if port 587 is blocked on your account, use a relay that offers an alternate submission port such as 2525. Email is optional. Without it, you invite users by generating a link in the admin UI and sending it yourself.
Backups
Kavita runs its own scheduled backup task. Configure frequency and retention under Server Settings > Tasks. Archives land in /opt/kavita/config/backups. Copy them off the server.
rsync -avP root@your-vps-ip:/opt/kavita/config/backups/ /backups/kavita/For a full cold backup including the cover cache:
cd /opt/kavita
docker compose stop
tar czf /root/kavita-config-$(date +%F).tar.gz config
docker compose startThe library itself is not in these backups. Keep your originals elsewhere.
Upgrading
cd /opt/kavita
docker compose pull
docker compose up -d
docker image prune -fIf you pinned a tag, update it in compose.yaml first. Kavita migrates its database automatically on startup and the migration does not run backward. Take a backup before every upgrade and read the release notes, which call out breaking changes clearly.
Troubleshooting
Library scans but shows zero series. Files are sitting directly in the library root. Move them into one folder per series and rescan.
One series splits into several, or volumes appear out of order. Inconsistent filenames. Check that every file in the folder uses the same series name and volume or chapter pattern. A ComicInfo.xml with the correct Series field also fixes grouping.
Scan progress bar never moves and pages need a manual refresh. The SignalR WebSocket is not passing through the proxy. Confirm the Upgrade and Connection headers survived Certbot's edit with nginx -T | grep -A2 Upgrade.
Permission denied in the logs during a scan. The container cannot read the file. Run chmod -R a+rX on the library path, then rescan.
Locked out after changing Base URL. Edit BaseUrl in /opt/kavita/config/appsettings.json back to /, then docker compose restart.
High CPU right after adding a large batch of files. Cover generation. It is a one-time cost per file and settles once the scan finishes.
Hardening Notes
- Confirm the container is only on loopback:
ss -tlnp | grep 5000should show127.0.0.1. - Keep library mounts read-only.
- Treat OPDS URLs as credentials. Rotate API keys for users who leave.
- Use key-based SSH and disable password login.
- For a private family server, place it behind WireGuard or Tailscale and drop the public DNS record.
Next Steps
Kavita and Komga cover much of the same ground. Kavita is lighter on memory and handles EPUB-heavy collections well. Komga has deeper comic metadata editing and Kobo sync. Both have their own RamNode guides, and nothing stops you running both against the same read-only library to compare. For audiobooks and podcasts, add Audiobookshelf alongside either one on a separate subdomain.
