[Blog](https://deploywithox.com/blog) [Guides](https://deploywithox.com/blog#c-guides) # How to self-host Kavita reading server on your own VPS Run Kavita v0.9.1.4 on your own Ubuntu server with ox: an ASP.NET Core build, the Angular reader, SQLite and a first admin, from the config that ran. Saurav Sharma Oct 17, 2026 13 min read On this page - [What you need](https://deploywithox.com/blog/self-host-kavita#what-you-need) - [The steps](https://deploywithox.com/blog/self-host-kavita#steps) - [The ox.toml, line by line](https://deploywithox.com/blog/self-host-kavita#config) - [What it runs on](https://deploywithox.com/blog/self-host-kavita#services) - [Updates and backups](https://deploywithox.com/blog/self-host-kavita#updates) - [Frequently asked questions](https://deploywithox.com/blog/self-host-kavita#faq) To self-host Kavita you need an Ubuntu server, a fork of the Kavita repo with an `ox.toml` and two small files added, and four variables: the first admin's user name, password and email, and a time zone. In our run the first deploy took about 10 minutes, which covers building the .NET server and the Angular reader on the server, and ox set up no database server, because Kavita keeps its data in SQLite files. **How we tested** On Oct 17, 2026, ox deployed Kavita v0.9.1.4 from commit [`879b8b73`](https://github.com/Kareadita/Kavita/commit/879b8b73eb41d66f88dde8eb874cc99a3397eb11) on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory. The run then redeployed it, backed it up and rebooted the server. Then a separate test agent, one that did not write the config, signed in, added a book library, saw the scan list its book, and opened the book in the reader, and checked the app's services and its backup. [Kavita](https://github.com/Kareadita/Kavita) is a reading server for books and comics. It scans folders of EPUB and PDF books and of comics in CBZ, CBR, 7-zip and image formats, and opens them in browser readers that work on a phone, tablet or desktop. Its README says to treat it as beta software until 1.0.0, and the code is under the GPL 3. The server is an ASP.NET Core program with an Angular interface. ox deployed Kavita v0.9.1.4 (commit 879b8b73) on Oct 17, 2026 on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory; the first deploy took 10 minutes and a redeploy took 8 minutes. - **First deploy:** 10 minutes, from adding the project until the app passed the run's first checks. - **Redeploy of the same commit:** 8 minutes, until the app passed the checks again. - **Server:** DigitalOcean, 8 vCPUs and 16 GB of memory (`s-8vcpu-16gb`), with this app alone on it. Every one of the 85 apps we deployed ran on this one size, so it was not picked for this app. - **Memory in use after the reboot:** 815 MB of 15986 MB, for the whole server: Ubuntu, the app, its services, Caddy and the ox agent. On a server with no apps, ox's tests on real servers hold the agent to 40 MB or less and Caddy to 50 MB or less. - **Source:** [github.com/Kareadita/Kavita](https://github.com/Kareadita/Kavita) at v0.9.1.4, commit [`879b8b73`](https://github.com/Kareadita/Kavita/commit/879b8b73eb41d66f88dde8eb874cc99a3397eb11). The Kavita sign-in page The Kavita series page of the sample book Ox Sample after the library scan The first chapter of the sample book open in the Kavita reader ## What you need - An Ubuntu LTS server. The facts above give the size the run used and the memory it measured after a reboot. - A GitHub account to fork `Kareadita/Kavita`, and an ox account, which you open by signing in with GitHub. - A user name and a password for the first admin. Kavita sends invites and password resets by email. The run set up no mail server, so email was not tested. ## The steps On the ox dashboard, Add a server gives a one-line install command with a fresh token in it. Run it as root on your Ubuntu server. Fork `Kareadita/Kavita` and start from the v0.9.1.4 release commit, `879b8b73` in the box above. Add the three files shown below at the paths shown: `ox.toml` at the root, and `ox/start.sh` and `ox/sample-book.py` in a new `ox` folder. Commit and push. Then write a file named `kavita.env` with one `KEY=value` line for each key under the variables heading. With the [ox CLI](https://deploywithox.com/docs/cli) signed in, run: Shell ```bash ox new /Kavita ox review kavita --from-file kavita.env --wait ``` The project is named after the repo in lower case, `kavita`. The review saves the values and starts the first deploy, and `--wait` prints the log until it ends. The [quickstart](https://deploywithox.com/docs/quickstart) shows the browser version. When the deploy is green, `ox status kavita` shows the project's address. Open it and sign in with `KAVITA_ADMIN_USERNAME` and `KAVITA_ADMIN_PASSWORD`. The test agent checked that a wrong password is refused ("Your credentials are not correct") and that the admin's own password gets in. A new Kavita has no library. The test agent opened Settings, then Libraries, pressed Add Library, named it, chose the type Book, browsed to `/srv/ox/kavita/data/library` on the run's server, and saved. Kavita scanned the folder at once and listed one series, Ox Sample, which was still there after a reload. Read opened the first chapter in the web reader. The run served Kavita on its preview address only, so a domain of your own was not part of it; the [domains page](https://deploywithox.com/docs/operate/domains) covers adding one. Other users, the OPDS feed for reading apps, and Kavita+ were not tested. ## The ox.toml, line by line TOML ```toml # Kavita: an ASP.NET Core server (Kavita.Server) that serves its Angular UI # (UI/Web) from wwwroot. Mirrors build.sh (BuildUI, then Package's publish) # and the Docker image: Kavita runs from its publish folder, out/, and keeps # its data in out/config. # The Dockerfile's runtime packages (libvips needs libgdiplus and jemalloc). packages = ["libicu-dev", "libgdiplus", "curl", "tzdata", "libjemalloc2"] # Kavita makes its first admin only through its running server (POST # /api/account/register, open until an admin exists). The start makes it from # these, so the deploy ends with an admin the operator signs in with. required_variables = ["KAVITA_ADMIN_USERNAME", "KAVITA_ADMIN_PASSWORD"] [tools] # global.json and the release workflow: .NET 10, Node 24 dotnet = "10" node = "24" [build] install = "cd UI/Web && npm ci --legacy-peer-deps" commands = [ "cd UI/Web && npm run prod", "dotnet publish Kavita.Server/Kavita.Server.csproj -c Release -o out", "rm -rf out/BuildHost-net472 out/BuildHost-netcore", "mkdir -p out/wwwroot && cp -R UI/Web/dist/browser/. out/wwwroot/", # The test's sample book: the first deploy seeds it into the kept library. "python3 -I ox/sample-book.py 'library/Ox Sample/Ox Sample.epub'", ] # Kavita runs its EF Core migrations itself as it starts (after a backup). migrate = "" [app] start = "exec bash ox/start.sh" health = "/api/health" # One SQLite database (config/kavita.db) and an in-process Hangfire scheduler: # two copies must never run against one config folder. stop_before_start = true [storage] # out/config: kavita.db, appsettings.json (its JWT key), covers, bookmarks, # logs, backups, themes. library: a place for comics, manga and books (one # folder per series), added as a library folder in Kavita's settings. keep = ["out/config", "library"] ``` The config follows Kavita's `build.sh` and Dockerfile, and Kavita runs from a publish folder named `out`. - `packages` is the apt line of the Dockerfile's runtime image. Its entrypoint preloads `libjemalloc2` and warns of higher memory use without it, and `tzdata` backs `TZ`. - `required_variables` names the first admin's two keys, which ox asks for before the first deploy. Kavita's register call is open to anyone until an admin exists, so the start script makes the admin from your values and the first visitor cannot claim it. - `[tools]` pins .NET 10 and Node 24, the versions Kavita's `global.json` and workflows use. `install` runs `npm ci` in `UI/Web` with the flag `build.sh` uses. - The build `commands` run in order: `npm run prod` builds the interface, and `dotnet publish` puts a Release build of `Kavita.Server` in `out`. The next line removes the two `BuildHost` folders from that output, and the interface is copied to `out/wwwroot`. The last command writes the sample book. - `migrate` is empty, because Kavita applies its own database migrations as it starts. - `start` runs the script below. `health` is `/api/health`, the path the Dockerfile's health check calls, and ox switches to a new release only once it answers. - `stop_before_start = true` stops the running copy before the new one starts, because two copies must not share one SQLite database and one scheduler. The cost is a short gap on each deploy, when the address answers 502. - `keep` carries two folders across deploys. `out/config` holds the database, `appsettings.json`, covers, bookmarks, logs and themes. `library` is the folder for books. ox backs both up. There are no workers and no cron jobs. Kavita's scheduler runs inside the app, and the test agent saw it work in the log, in a library scan and a cache purge. `ox/sample-book.py` Code ``` """Write a small EPUB 3 book to the path in argv[1]. Kavita has no upload: a person puts books in a folder on the server and adds that folder as a library. The test ships this book in library/, which ox seeds into the kept folder on the first deploy, so a new library there has a book to scan and open. The text is written for this test; the file is the same on every build. """ import os import struct import sys import zlib import zipfile STAMP = (2026, 1, 1, 0, 0, 0) TITLE = "Ox Sample" BOOK_ID = "urn:uuid:5f1c2a64-6c1e-4d3b-9a57-0b6f0e7d2a10" CHAPTERS = [ ( "Chapter 1: A book to open", [ "This small book ships with the test of Kavita on ox.", "Kavita reads books from folders on the server. A library is one" " such folder, and each folder inside it is a series.", "If you can read this page, the library scan found the book and" " the reader opened it.", ], ), ( "Chapter 2: Your own books", [ "To add your own books, copy them into the library folder on the" " server, one folder per series, and scan the library again.", "Kavita reads EPUB and PDF books, comics in CBZ, CBR, CB7 and" " CBT archives, and folders of images.", ], ), ] def png(width, height, rgb): def chunk(kind, data): body = kind + data return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body)) row = b"\x00" + bytes(rgb) * width return ( b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(row * height, 9)) + chunk(b"IEND", b"") ) def page(title, body): return ( '\n' '\n' f"{title}\n" f"\n{body}\n\n\n" ) def main(): out = sys.argv[1] files = {} files["META-INF/container.xml"] = ( '\n' '\n' '\n' "\n" ) items, spine, nav, ncx = [], [], [], [] for i, (heading, paragraphs) in enumerate(CHAPTERS, 1): name = f"chapter-{i}.xhtml" body = f"

{heading}

\n" + "\n".join(f"

{p}

" for p in paragraphs) files[f"OEBPS/{name}"] = page(heading, body) items.append(f'') spine.append(f'') nav.append(f'
  • {heading}
  • ') ncx.append( f'{heading}' f'' ) files["OEBPS/nav.xhtml"] = page( TITLE, '" ) files["OEBPS/toc.ncx"] = ( '\n' '\n' f'\n' f"{TITLE}\n" "" + "".join(ncx) + "\n\n" ) files["OEBPS/cover.png"] = png(300, 450, (0x51, 0x9D, 0xFA)) files["OEBPS/content.opf"] = ( '\n' '\n' '\n' f'{BOOK_ID}\n' f"{TITLE}\n" "ox\n" "en\n" '2026-01-01T00:00:00Z\n' '\n' "\n\n" '\n' '\n' '\n' + "\n".join(items) + '\n\n\n' + "\n".join(spine) + "\n\n\n" ) os.makedirs(os.path.dirname(out), exist_ok=True) with zipfile.ZipFile(out, "w") as z: # The mimetype comes first and is stored, not compressed (OCF). z.writestr(zipfile.ZipInfo("mimetype", STAMP), "application/epub+zip", zipfile.ZIP_STORED) for name, data in files.items(): z.writestr(zipfile.ZipInfo(name, STAMP), data, zipfile.ZIP_DEFLATED) main() ``` `ox/start.sh` Code ``` #!/usr/bin/env bash # Start Kavita on ox. The published server is in out/, and out/config (kept) # holds appsettings.json, kavita.db, covers, logs and the rest of its data. # Kavita reads its config folder from the working directory. set -euo pipefail repo=$PWD cd out # The Docker entrypoint's allocator for libvips. jemalloc=$(ldconfig -p | grep -m1 'libjemalloc.so.2' | awk '{print $NF}') || true if [ -n "$jemalloc" ]; then export LD_PRELOAD="$jemalloc"; fi # The Docker entrypoint's first run: copy the default settings in. if [ ! -f config/appsettings.json ]; then cp "$repo/Kavita.Server/config/appsettings.json" config/appsettings.json fi # Kavita takes its port and listen address only from appsettings.json. listen() { sed -i -E \ -e "s/\"Port\": *[0-9]+/\"Port\": $PORT/" \ -e "s/\"IpAddresses\": *\"[^\"]*\"/\"IpAddresses\": \"$1\"/" \ config/appsettings.json } # Kavita makes its first admin only through its running server: start it on # another loopback address (so ox's health check never sees this copy), make # the admin when none exists, stop it, then start it for real. listen 127.0.0.2 ./Kavita.Server & pid=$! base="http://127.0.0.2:$PORT" for _ in $(seq 1 300); do curl -fsS -o /dev/null "$base/api/health" 2>/dev/null && break kill -0 "$pid" 2>/dev/null || { wait "$pid" || true; echo "Kavita exited before it answered"; exit 1; } sleep 1 done exists=$(curl -fsS "$base/api/admin/exists") if [ "$exists" = false ]; then # The values go through the environment and stdin, never a command line. answer=$(python3 -I -c 'import json, os; print(json.dumps({"username": os.environ["KAVITA_ADMIN_USERNAME"], "email": os.environ.get("KAVITA_ADMIN_EMAIL", ""), "password": os.environ["KAVITA_ADMIN_PASSWORD"]}))' | curl -sS -w '\n%{http_code}' -H 'Content-Type: application/json' --data-binary @- "$base/api/account/register") if [ "${answer##*$'\n'}" != 200 ]; then echo "Kavita refused the admin $KAVITA_ADMIN_USERNAME: ${answer%$'\n'*}" | head -c 400 echo kill "$pid" wait "$pid" || true exit 1 fi echo "Made the Kavita admin $KAVITA_ADMIN_USERNAME" elif [ "$exists" != true ]; then echo "Kavita's /api/admin/exists answered: $exists" kill "$pid" wait "$pid" || true exit 1 fi kill "$pid" wait "$pid" || true listen 127.0.0.1 exec ./Kavita.Server ``` `start.sh` does the Docker entrypoint's job, plus the first admin: - Kavita reads its port from `config/appsettings.json`. The script copies the repo's default file there on the first start, as the entrypoint does, and writes `$PORT` into it before every start. The Dockerfile's `DOTNET_RUNNING_IN_CONTAINER=true` would pin Kavita to port 5000 whatever the file says, so the config leaves it unset. - It starts Kavita once on `127.0.0.2`, an address ox's health check never asks, and reads `/api/admin/exists`. When the answer is false it posts `/api/account/register` with the user name, email and password, taken from the environment and sent on stdin, never on a command line. Then it stops that copy and starts the real one on `127.0.0.1`. When an admin exists it posts nothing, and if Kavita refuses the admin the deploy stops and prints Kavita's answer. `sample-book.py` writes a small EPUB, `Ox Sample`, into `library/` during the build. A kept folder starts with what the build left in it, so the first deploy seeds the book into the kept library, and a new Kavita library has something to scan. Shell ```bash TZ KAVITA_ADMIN_USERNAME KAVITA_ADMIN_EMAIL KAVITA_ADMIN_PASSWORD ``` `KAVITA_ADMIN_USERNAME` and `KAVITA_ADMIN_PASSWORD` are the admin's name and password, and you choose both. `KAVITA_ADMIN_EMAIL` is the admin's address. The config does not require it, the run set it, and Kavita uses the user name in its place when it is empty. `TZ` is the time zone name, such as `Europe/Berlin`. Changing the admin keys later does not change the account. See [variables](https://deploywithox.com/docs/operate/variables) for how ox stores values. ## What it runs on Kavita needed no service from ox. The run found one systemd unit, the app, with no restarts. ## Updates and backups To move to a new release, merge its tag into your fork and push, or run `ox deploy kavita --wait`. A newer version was not tested. Kavita's update check may call this release out of date, because the v0.9.1.4 tag still carries the version number 0.9.1.3. Backups need no setup. The run's backup, made with Back up now, held four files: a tar of `out/config`, a tar of `library`, and compressed copies of `kavita.db` and `cache.db`. Daily backups ran too. Run `ox backups kavita` to list them, or open the Backups tab. A restore was not tested. The [backups page](https://deploywithox.com/docs/operate/backups) covers keeping copies in an S3 bucket. ## Frequently asked questions ### Do I need Docker to self-host Kavita? No. Kavita's repo has a Dockerfile, and its README points to a wiki with several install methods, but the app is a published .NET program and a folder of web files. ox builds both from your fork on the server and runs the program as a system service. ### How do I create the first Kavita admin account? Set KAVITA_ADMIN_USERNAME and KAVITA_ADMIN_PASSWORD before the first deploy. The start script registers that admin through Kavita's own first-user call before the site is reachable, and you sign in on the login page. Other users join by invite in Settings, which needs email and was not tested. ### Which database does Kavita need? None that you install. Kavita keeps its data in a SQLite file, kavita.db, with a small cache.db beside it, both in its config folder, so ox set up no Postgres. ox backs each file up as an entry of its own. ### How do I add my own books to Kavita? Kavita reads books from folders on the server, one folder per series, and a library is a folder you add in Settings. The run used one sample book in the kept library folder. Copying your own files in and scanning them was not tested. ox keeps that folder across deploys and backs it up. ### How much memory does self-hosted Kavita use? The facts above give what the run used: the whole server after a reboot, counting Kavita, Caddy and the ox agent, before the test agent added its library. Scanning a large collection was not measured. ox takes any server from 1 GB up and says so when memory stopped an app. New to running apps on your own server? [How ox self-hosts your apps](https://deploywithox.com/self-hosting) shows what ox sets up on your server. On this page - [What you need](https://deploywithox.com/blog/self-host-kavita#what-you-need) - [The steps](https://deploywithox.com/blog/self-host-kavita#steps) - [The ox.toml, line by line](https://deploywithox.com/blog/self-host-kavita#config) - [What it runs on](https://deploywithox.com/blog/self-host-kavita#services) - [Updates and backups](https://deploywithox.com/blog/self-host-kavita#updates) - [Frequently asked questions](https://deploywithox.com/blog/self-host-kavita#faq) - [Do I need Docker to self-host Kavita?](https://deploywithox.com/blog/self-host-kavita#faq-docker) - [How do I create the first Kavita admin account?](https://deploywithox.com/blog/self-host-kavita#faq-admin) - [Which database does Kavita need?](https://deploywithox.com/blog/self-host-kavita#faq-database) - [How do I add my own books to Kavita?](https://deploywithox.com/blog/self-host-kavita#faq-books) - [How much memory does self-hosted Kavita use?](https://deploywithox.com/blog/self-host-kavita#faq-memory) Written by Saurav Sharma Builds ox. Writes about running apps on servers you own, without Docker and without a platform bill. [Older post How to self-host Kellnr, a Rust registry, on your own VPS](https://deploywithox.com/blog/self-host-kellnr) [Newer post How to self-host Karakeep (bookmarks) on your own VPS](https://deploywithox.com/blog/self-host-karakeep) ## Keep reading - Guides [Self-Hosted Open-Source Apps Deployed with ox: All 85](https://deploywithox.com/blog/self-hosted-open-source-apps-deployed-with-ox) ox deployed 85 open-source apps from pinned commits on fresh servers, with no Docker. The stacks, services, deploy times and a table of every app. Saurav Sharma Oct 17, 2026 16 min read - Guides [How to self-host AFFiNE on your own VPS without Docker](https://deploywithox.com/blog/self-host-affine) Run AFFiNE v0.27.4 on your own Ubuntu server with ox: Postgres with pgvector, Redis, a Rust and Node build, and an admin made from two variables. Saurav Sharma Oct 17, 2026 12 min read - Guides [How to self-host Akaunting on your own VPS without Docker](https://deploywithox.com/blog/self-host-akaunting) Run Akaunting 3.2.4 on your own Ubuntu server with ox: PHP, MariaDB, a scheduler and the first company and admin made on deploy, from the config that ran. Saurav Sharma Oct 17, 2026 10 min read ## Deploy to a server you own ox sets up systemd, Caddy and PostgreSQL on your Ubuntu server and deploys on every push. Free for your first project. [Sign up with GitHub](https://deploywithox.com/login/github) [Read the docs](https://deploywithox.com/docs)