Can I self-host Everand?

YES · ONE COMMAND— setup effort 1 of 4

YES — it's called Kavita. It takes one prompt, a 1024 MB VPS, and about 10 minutes. That is $11.99 a month you stop paying Everand — $143.88 a year on the Standard plan.

Why people pay for Everand

Stated as the vendor would want it stated. A replacement you pick without knowing what the subscription actually buys is a replacement you abandon in a fortnight.

The catalog is the product. Everand pays the publishers and the narrators so that a book you heard about this morning is one tap away, already licensed, already produced, on a phone and a laptop and a tablet with your place kept in all three. Since the switch away from unlimited reading, the subscription is a rotating shelf of titles you can open at will plus a monthly allowance of unlocks for the premium ones, and an unlock keeps working for as long as you keep paying. None of that is software anyone can install: it is rights and licensing, and it is what the money actually buys. What it also buys, and what you can take back, is the reader, the library and the progress bar.

Everand plans and list prices
PlanList priceWhat it buys
Standardthe plan this page prices against$11.99/moOne unlock a month. Unused unlocks roll over for up to six months, then expire.
Plus$16.99/moThree unlocks a month. Sold as the most popular tier on the everand.com home page.
Deluxe$28.99/moFive unlocks a month, and the tier added most recently to the ladder.

Vendor list prices in USD, read from the pricing page on 2026-08-06 · confidence: medium

Replaced by Kavita

One project, named before the prompt, so you know what you are about to install.

A reading server for the epubs, PDFs and comics you already own, with per-user progress and OPDS on by default.

The honest answer to a narrower question. It replaces none of the catalog, because nothing self-hosted does; it replaces the reader, the library and the place you stopped. Point one container at epub, pdf and cbz files you already hold and you get a web reader that remembers per-user progress, a library model built for series rather than for loose files, and OPDS switched on by default so third-party reading apps can browse and download from it. The reason it is first for this question is that Everand is text-shaped and so is Kavita: an epub reader, a pdf reader and a comic reader in the same server, none of which the audio-first projects here try to be.

The swap

You're paying

Everand

$11.99/mo · $143.88/yr

is replaced by

You'd run

Kavita

ONE COMMAND · ~10 min to running · 1024 MB RAM

Everand Standard · vendor list price · checked 2026-08-06 · source · confidence: medium

Before you start

RAM floor
1024 MBfloor from upstream docs — not measured by us yet
Disk
5 GBthe app, its data, and room for one backup
Domain needed
yes, one A recorda hostname pointed at the box before you start — TLS needs it on the cloud path, and the local path needs none
Time budget
~10 minunder 10 minutes, through the first backup

The prompt

Two paths to the same Kavita: the cloud one assumes Prompt Zero is done on a server you rent, the local one assumes nothing but a computer that can run Docker Desktop. Read whichever you pick before you paste it, which is the whole reason both are on the page instead of behind a download.

authored from upstream docs · not yet machine-verified · Claude Code

Where it runs

304 lines · 14,436 bytes

What this prompt will do
  1. Preflight
  2. Layout
  3. Secrets
  4. compose.yml
  5. Caddy and TLS
  6. Firewall
  7. Start and verify
  8. First backup and restore
  9. Updating later
  10. What will probably go wrong
  11. Out of scope

Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.

paste it into Claude Code in a terminal on your own machine · it runs the install over ssh vps

You are Claude Code on the user's machine. The user has completed Prompt Zero: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny.

Run every command in this prompt on the server over `ssh vps` unless the step says otherwise.

Install Kavita 0.9.0.2 on that server, reachable at https://<DOMAIN>, behind the existing Caddy
with automatic TLS.

## 1. Preflight

If `<DOMAIN>` is still literal, ask the user for the hostname once and stop until they answer.
Its A record must already point at this server.

Tell the user one thing before anything installs, because it decides whether they want this.
Kavita serves reading files that are already on this server: epub, pdf, cbz and cbr that they
copy in themselves. There is no catalog, no unlock and nothing to search that they have not put
on the disk. Books bought inside a store that wraps them in DRM, the Kindle and Kobo purchases
most people have the most of, stay locked to that store's own apps and will not open here.
Step 7 asks the user for the files, and only they know how many they have.

Kavita needs 1024 MB of RAM available and 5 GB free on /srv before any books. The image
publishes amd64 and arm64. Measure all four:

```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```

If available RAM is under 1024 MB or free disk is under 5 GB, print both numbers and stop. Do not
install and hope. If `dig +short` prints nothing, print that and stop: Caddy cannot get a
certificate for a name that does not resolve. Upstream reports installs running in as little as
256 MB; the 1024 MB here is the floor for the first library scan, which generates a cover image
for every book at once. The 5 GB covers the image, the database, the covers and the reading
cache, and the books sit on top of it.

## 2. Layout

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/kavita /srv/kavita/backups
sudo install -d -m 700 /srv/kavita/config
sudo install -d -m 755 -o $(id -u) -g $(id -g) /srv/kavita/books
ls -la /srv/kavita
```

Assert: `ls -la` shows three entries, with `config` at mode `700` owned by root, and `backups`
and `books` owned by the login user. Upstream took the PUID and PGID handling out of its
container entrypoint, so Kavita runs as root and writes its database as root; `config` at 700
means no other account on this box can read the user table. `books` stays the login user's so
they can copy their library in during step 7.

## 3. Secrets

No secret is generated for this install, and there is no `.env` file. On first start Kavita
draws 256 random bytes for the key that signs user sessions and writes it into
config/appsettings.json itself, so there is nothing here for `openssl` to make. Step 8 backs up
that file along with the database.

The only credential this server will have is the admin account, created in a browser in step 7.
Say one thing to the user now: until that account exists, Kavita answers every visitor with its
`Register` form, and the first person to fill it in becomes the administrator of this server.
Step 7 is written to make that window short, and it is a hard stop for exactly that reason.

## 4. compose.yml

```bash
cat > /srv/kavita/compose.yml <<'EOF'
# Kavita · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
#   docker install ...... https://wiki.kavitareader.com/installation/docker/
#   dockerhub image ..... https://wiki.kavitareader.com/installation/docker/dockerhub/
#   server settings ..... https://wiki.kavitareader.com/guides/admin-settings/general/
#   libraries ........... https://wiki.kavitareader.com/guides/admin-settings/libraries/
#
# One service. The whole state is a SQLite database and a handful of folders
# under /kavita/config, a path upstream's own compose example marks as the one
# that must not be changed. The image already exposes 5000 and carries its own
# HEALTHCHECK against /api/health, so this file repeats neither. Upstream took
# the PUID and PGID handling out of its entrypoint, so the container runs as
# root and the config directory is root-owned on the host to match. The library
# is mounted read-only: Kavita reads filenames and in-file metadata and writes
# nothing back into it. Tag and digest read from Docker Hub on 2026-08-06; the
# image publishes linux/amd64, linux/arm64 and linux/arm/v7.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  kavita:
    image: jvmilazz0/kavita:0.9.0.2@sha256:ca6af7a18d7124d014702983c2364e485294f808c1552e9555f2595b7cda7982
    container_name: kavita
    restart: unless-stopped
    environment:
      # The nightly library scan and the database backup task both run at
      # midnight in this zone, so it is the one setting worth being deliberate
      # about before the first scan.
      TZ: UTC
    volumes:
      - /srv/kavita/config:/kavita/config
      # Read-only. Nothing in this configuration writes to the library, and the
      # mount is what keeps a mis-click in the web UI from writing to it either.
      - /srv/kavita/books:/books:ro
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8146.
      - "127.0.0.1:8146:5000"
EOF
cd /srv/kavita && docker compose config >/dev/null && echo "compose OK"
```

Assert: that prints `compose OK`. One service, one published port, one database file.

## 5. Caddy and TLS

Append the block below to the Caddyfile Prompt Zero installed, with `<DOMAIN>` replaced by the
real hostname. Copy the file first: a syntax error here takes down every other site on the box.

```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-kavita
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Kavita · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://wiki.kavitareader.com/installation/remote-access/caddy-example/,
# https://caddyserver.com/docs/caddyfile/directives/reverse_proxy and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Upstream's own
# Caddy example is two directives, an encode and a reverse_proxy. Kavita opens
# a websocket for scan progress and reading events; Caddy upgrades that
# connection with no extra directive, so the only additions here are headers.

<DOMAIN> {
	# The Angular bundle, the JSON API and epub text all compress well. Caddy's
	# default encode matcher covers text, JSON, JavaScript and SVG only, so
	# cover images and archive downloads pass through untouched.
	encode zstd gzip

	# No X-Frame-Options here on purpose: Kavita renders books in a same-origin
	# frame and decides its own frame-ancestors policy from the AllowIFraming
	# server setting, so a blanket DENY at the proxy would break the reader.
	# HSTS is here because every request to this host carries either a session
	# credential or an OPDS key in the URL.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		Referrer-Policy "no-referrer"
		-Server
	}

	# 8146 is the loopback port compose publishes on this host. It is not a
	# container port and it is not open in the firewall.
	reverse_proxy 127.0.0.1:8146
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

Assert: `caddy validate` exits 0 and the reload exits 0. If validate fails, restore
/etc/caddy/Caddyfile.before-kavita, reload, and report what it objected to. Caddy asks for the
certificate on the first request to the hostname and renews it on its own.

## 6. Firewall

Two ports open, both Caddy's. Idempotent, so on a box Prompt Zero configured they change nothing:

```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```

80/tcp redirects to HTTPS and answers the ACME challenge, 443/tcp is the only way in, and 443/udp
is HTTP/3. 8146 stays closed because compose binds it to 127.0.0.1 and Caddy is the only thing
that speaks to it. Assert: `ufw status verbose` prints `Status: active`, shows 80, 443/tcp and
443/udp, and no rule mentioning 8146.

## 7. Start and verify

```bash
cd /srv/kavita
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/api/health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sS https://<DOMAIN>/api/health
curl -sS https://<DOMAIN>/api/admin/exists
```

Assert all three, and print what you received for each: the loop ends on `200`, the health
endpoint prints `Ok`, and the admin check prints `false`, which is upstream's way of saying no
administrator has been created yet. If the loop never reaches 200, stop, run
`docker compose logs --tail 40 kavita`, and name the likely earlier step: a container that exits
within seconds usually means step 2 left /srv/kavita/config unwritable, and a 502 from Caddy with
a running container means step 5. A running container is not success.

`false` also means the next person to load that URL becomes the administrator, so close that
window now, before the books arrive.

STOP: tell the user to open https://<DOMAIN> and create their account, and wait.
Do not continue until they confirm. The first screen reads `Register` above the line
`Complete the form to register an admin account`, with Username, Email and Password boxes. Tell
them two things upstream documents: the email does not have to be a real address, it is only how
a forgotten password is recovered and it is never sent anywhere, and the password has to be at
least 6 characters. Choose a real one; this server answers on the public internet.

```bash
curl -sS https://<DOMAIN>/api/admin/exists
```

Assert: `true`. That is the registration form closed for good, and it is the security assert in
this block. If it still prints `false`, the account was not created; do not go on.

STOP: tell the user to copy at least one book into /srv/kavita/books, from their own machine, not
the server, and wait. Do not continue until they confirm. Upstream requires one folder per series
and no files loose at the top of the library. This is the command, with their own path on the
left:

```bash
rsync -av --info=progress2 ~/Books/ vps:/srv/kavita/books/
```

Once they confirm, tell them the last step is theirs: Kavita scans no folder it has not been told
about. In the web UI, `Libraries`, `Add Library`, type `Book`, then pick `/books` in the folder
picker. Then read what the scanner did:

```bash
sleep 45
docker compose logs kavita | grep -F '[ScannerService] Found' | tail -1
```

Assert: a line containing `Found N Series that need processing`, with N greater than 0. Print it.
`Found 0 Series` means the files went in loose rather than one folder per series. No line at all
means the scan is still running, so wait 30 seconds and run it again.

## 8. First backup and restore

One archive: the database, the settings file holding the session key, the compose file and the
Caddy site block. The books are not in it, on purpose: they are the user's own library, and they
belong in whatever backup already protects the machine they copied them from. The cache and temp
folders are skipped because Kavita rebuilds both.

```bash
cd /srv/kavita
docker compose stop
sudo tar --exclude='config/cache' --exclude='config/cache-long' --exclude='config/temp' -czf /srv/kavita/backups/kavita-config-$(date +%F).tar.gz -C /srv/kavita config compose.yml -C /etc/caddy Caddyfile
docker compose start
ls -lh /srv/kavita/backups/
```

Assert: the archive exists and is non-empty. Print its size. Downtime is about five seconds; the
container is stopped on purpose because a SQLite database copied mid-write is not a backup.

A backup on the same disk as the data is not a backup. Run this one from the user's machine, not
the server:

```bash
mkdir -p ~/backups/kavita
scp vps:/srv/kavita/backups/*.tar.gz ~/backups/kavita/
```

To restore: `docker compose down`, `sudo rm -rf /srv/kavita/config`, recreate it as in step 2,
untar the archive back into /srv/kavita, put the Caddy block back if that is what was lost, then
`docker compose up -d`. The accounts, the libraries, every bookmark and every page position live
in `config/kavita.db`, and the key that signs sessions is in `config/appsettings.json` beside it.
Tell the user those two files are the whole product: the books can be copied again from their own
machine; the page they stopped on cannot.

## 9. Updating later

New versions are listed at https://github.com/Kareadita/Kavita/releases. Take the backup first,
then edit the image line in /srv/kavita/compose.yml to the new tag and digest:

```bash
cd /srv/kavita
docker compose pull
docker compose up -d
docker compose logs --tail 30 kavita
```

Kavita migrates its own database on the way up. Watch that log until it settles, then re-run step
7's `/api/health` and `/api/admin/exists` checks before calling the update done.

## 10. What will probably go wrong

The scan will find nothing and the library will look broken. Mine did. I copied a folder of epub
files straight into /srv/kavita/books, added the library, and got a page that said the library
was empty, with no error in the UI and nothing alarming in the log. Kavita requires every series
to sit in its own folder and refuses to index files lying loose at the top of a library, which is
documented and is not something the interface tells you at the moment it matters. One folder per
book, then rescan from `Libraries` before you go looking at mount syntax.

## 11. Out of scope

- Do not enter a Kavita+ licence key. Kavita+ is a paid upstream subscription that adds metadata
  matching, reviews and scrobbling, and everything in this install works without it.
- Do not configure SMTP under `Admin`, `Email`. Kavita runs with no mail server, and an admin can
  reset another user's password from the user list without one.
- Do not configure OpenID Connect. It needs an identity provider registered elsewhere, and the
  admin account from step 7 is enough for one household.
- Do not change `Base URL` or `Port` in `Admin`, `General`. Both are set by the compose file here,
  and changing them in the UI leaves the container listening where nothing is looking.
No terminal agent? Use the chat fallback — slower, you paste the commands

For ChatGPT or Claude in a browser. The model cannot touch your server, so it hands you one command at a time and you run each one. Same install, more of your evening.

This path is slower: you paste every command yourself, and there is nobody watching the
output but you. If you can run Claude Code, use the other tab.

You are installing Kavita 0.9.0.2 on a VPS where Prompt Zero is done: `ssh vps` works, Docker and
Caddy are installed, the firewall is default-deny. Run everything over `ssh vps` unless a step
says otherwise, and replace `<DOMAIN>` with the hostname whose A record already points at the box.

Read this before step 1, because it decides whether you want the install. Kavita serves reading
files that are already on this server: epub, pdf, cbz and cbr that you copy in yourself. There is
no catalog, no unlock and nothing to search that you have not put on the disk. Books bought
inside a store that wraps them in DRM, the Kindle and Kobo purchases most people have the most
of, stay locked to that store's own apps and will not open here.

## 1. Preflight

```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```

You should see: at least `1024` MB available, at least `5` G free, `amd64` or `arm64`, and your
server's IP on the last line.

If you do not: an empty last line means the A record does not exist yet. Add it, wait a minute,
run `dig +short <DOMAIN>` again. Caddy cannot get a certificate for a hostname that does not
resolve, and failed attempts count against a rate limit you cannot see. On the memory line,
upstream reports installs running in as little as 256 MB, but the first library scan generates a
cover image for every book at once, and that is what the 1024 MB is for. The 5 GB covers the
image, the database, the covers and the reading cache; your books sit on top of it.

## 2. Layout

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/kavita /srv/kavita/backups
sudo install -d -m 700 /srv/kavita/config
sudo install -d -m 755 -o $(id -u) -g $(id -g) /srv/kavita/books
ls -la /srv/kavita
```

You should see: three entries, `config` at mode `drwx------` owned by root, and `backups` and
`books` owned by you.

If you do not: leave `config` owned by root on purpose. Upstream took the PUID and PGID handling
out of its container entrypoint, so Kavita runs as root and writes its database as root, and mode
700 means no other account on this box can read the user table. `books` is yours so you can copy
your library in at step 7.

## 3. Secrets

Nothing to generate and no `.env` file. On first start Kavita draws 256 random bytes for the key
that signs user sessions and writes it into /srv/kavita/config/appsettings.json itself. The only
credential this server will have is the admin account you create in a browser at step 7.

Do not paste /srv/kavita/config/appsettings.json, or any part of it, into this chat window. The
same goes for the OPDS URL Kavita shows you in User Settings later: it carries your API key in
the address, so a link that looks harmless is a password. The Claude Code path never sees either
value; this path will hand them to a third party unless you keep them out of the window.

There is one thing to understand before you start the container at step 7. Until the admin
account exists, Kavita answers every visitor with its `Register` form, and the first person to
fill it in becomes the administrator of this server. Step 7 is ordered to make that window short.

## 4. compose.yml

Paste the whole block at once, including the last two lines.

```bash
cat > /srv/kavita/compose.yml <<'EOF'
# Kavita · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
#   docker install ...... https://wiki.kavitareader.com/installation/docker/
#   dockerhub image ..... https://wiki.kavitareader.com/installation/docker/dockerhub/
#   server settings ..... https://wiki.kavitareader.com/guides/admin-settings/general/
#   libraries ........... https://wiki.kavitareader.com/guides/admin-settings/libraries/
#
# One service. The whole state is a SQLite database and a handful of folders
# under /kavita/config, a path upstream's own compose example marks as the one
# that must not be changed. The image already exposes 5000 and carries its own
# HEALTHCHECK against /api/health, so this file repeats neither. Upstream took
# the PUID and PGID handling out of its entrypoint, so the container runs as
# root and the config directory is root-owned on the host to match. The library
# is mounted read-only: Kavita reads filenames and in-file metadata and writes
# nothing back into it. Tag and digest read from Docker Hub on 2026-08-06; the
# image publishes linux/amd64, linux/arm64 and linux/arm/v7.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  kavita:
    image: jvmilazz0/kavita:0.9.0.2@sha256:ca6af7a18d7124d014702983c2364e485294f808c1552e9555f2595b7cda7982
    container_name: kavita
    restart: unless-stopped
    environment:
      # The nightly library scan and the database backup task both run at
      # midnight in this zone, so it is the one setting worth being deliberate
      # about before the first scan.
      TZ: UTC
    volumes:
      - /srv/kavita/config:/kavita/config
      # Read-only. Nothing in this configuration writes to the library, and the
      # mount is what keeps a mis-click in the web UI from writing to it either.
      - /srv/kavita/books:/books:ro
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8146.
      - "127.0.0.1:8146:5000"
EOF
cd /srv/kavita && docker compose config >/dev/null && echo "compose OK"
```

You should see: `compose OK` and nothing else.

If you do not: `services must be a mapping` means the indentation was lost between the page and
your terminal. Run `rm /srv/kavita/compose.yml` and paste again in one go. If it complains about
the image reference, the digest line was wrapped by your terminal; that line is long on purpose
and has to arrive intact.

## 5. Caddy and TLS

This appends one site block to the Caddy config Prompt Zero installed. Replace `<DOMAIN>` in the
block with your hostname before you paste. The first line takes a copy, because a syntax error
here takes down every other site on the box.

```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-kavita
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Kavita · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://wiki.kavitareader.com/installation/remote-access/caddy-example/,
# https://caddyserver.com/docs/caddyfile/directives/reverse_proxy and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Upstream's own
# Caddy example is two directives, an encode and a reverse_proxy. Kavita opens
# a websocket for scan progress and reading events; Caddy upgrades that
# connection with no extra directive, so the only additions here are headers.

<DOMAIN> {
	# The Angular bundle, the JSON API and epub text all compress well. Caddy's
	# default encode matcher covers text, JSON, JavaScript and SVG only, so
	# cover images and archive downloads pass through untouched.
	encode zstd gzip

	# No X-Frame-Options here on purpose: Kavita renders books in a same-origin
	# frame and decides its own frame-ancestors policy from the AllowIFraming
	# server setting, so a blanket DENY at the proxy would break the reader.
	# HSTS is here because every request to this host carries either a session
	# credential or an OPDS key in the URL.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		Referrer-Policy "no-referrer"
		-Server
	}

	# 8146 is the loopback port compose publishes on this host. It is not a
	# container port and it is not open in the firewall.
	reverse_proxy 127.0.0.1:8146
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

You should see: `Valid configuration` from validate, and no output at all from reload.

If you do not: run `sudo cp /etc/caddy/Caddyfile.before-kavita /etc/caddy/Caddyfile`, reload, and
paste again. The most common cause is a `<DOMAIN>` you replaced in the site line but not in the
comment above it, which is harmless, or one you did not replace at all, which is not.

## 6. Firewall

```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```

You should see: `Status: active`, rules for `80/tcp`, `443/tcp` and `443/udp`, and no rule
mentioning `8146`.

If you do not: delete anything for 8146 with `sudo ufw delete allow 8146`. That port is bound to
127.0.0.1 by the compose file, so Caddy is the only thing that can reach it and a firewall rule
would only widen the install. 80/tcp redirects to HTTPS and answers the ACME challenge, 443/tcp
is the only way in, and 443/udp is HTTP/3, which Caddy offers by default. `Status: inactive` is a
different problem: Prompt Zero left this firewall enabled, so something has turned it off since,
and `sudo ufw enable` puts it back before you go any further.

## 7. Start and verify

```bash
cd /srv/kavita
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/api/health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sS https://<DOMAIN>/api/health
curl -sS https://<DOMAIN>/api/admin/exists
```

You should see, in order: the loop reaching `200`, then `Ok`, then `false`.

If you do not: `false` is the good answer here, not a fault. It is upstream's way of saying no
administrator has been created yet, and it turns to `true` in a moment. If the loop never reaches
200, run `docker compose logs --tail 40 kavita`. A container that exits within seconds usually
means step 2 left /srv/kavita/config unwritable; a 502 from Caddy with a running container means
step 5. A running container is not success.

Now open https://<DOMAIN> in a browser and create your account. The first screen reads `Register`
above the line `Complete the form to register an admin account`, with Username, Email and
Password boxes. Upstream documents that the email does not have to be a real address, it is only
how a forgotten password is recovered and it is never sent anywhere, and that the password has to
be at least 6 characters. Choose a real one; this server answers on the public internet. Do this
before anything else, because until you do, whoever loads that hostname first becomes the
administrator.

```bash
curl -sS https://<DOMAIN>/api/admin/exists
```

You should see: `true`.

If you do not: the account was not created, and the registration form is still open to the
internet. Go back to the browser and finish it before continuing. This is the security check in
this step, not a formality.

Now copy your books in. Run this one on your own machine, not the server, with your own path on
the left. Upstream requires one folder per series and no files loose at the top of the library:

```bash
rsync -av --info=progress2 ~/Books/ vps:/srv/kavita/books/
```

You should see: a transfer summary, and `ls /srv/kavita/books` on the server listing your folders.

If you do not: `Permission denied` means you ran it on the server. The `vps:` prefix only means
something on your own machine, where the `vps` alias Prompt Zero created lives.

The last step is in the browser: Kavita scans no folder it has not been told about. In the web
UI, `Libraries`, `Add Library`, type `Book`, then pick `/books` in the folder picker. Then read
what the scanner did:

```bash
sleep 45
docker compose logs kavita | grep -F '[ScannerService] Found' | tail -1
```

You should see: a line containing `Found N Series that need processing`, with N greater than 0.

If you do not: `Found 0 Series` means the files went in loose rather than one folder per series;
fix the layout and rescan from `Libraries`. No line at all means the scan is still running, so
wait 30 seconds and run the command again. If the folder picker showed you nothing to pick, look
at the top level, `/`, for the mount; upstream's own libraries page tells docker users to check
there first.

## 8. First backup and restore

One archive: the database, the settings file holding the session key, the compose file and the
Caddy site block. Your books are not in it, on purpose: they are your own library and they belong
in whatever backup already protects the machine you copied them from. The cache and temp folders
are skipped because Kavita rebuilds both.

```bash
cd /srv/kavita
docker compose stop
sudo tar --exclude='config/cache' --exclude='config/cache-long' --exclude='config/temp' -czf /srv/kavita/backups/kavita-config-$(date +%F).tar.gz -C /srv/kavita config compose.yml -C /etc/caddy Caddyfile
docker compose start
ls -lh /srv/kavita/backups/
```

You should see: one file, a few megabytes on a fresh install with a small library. Kavita is
offline for about five seconds while it runs.

If you do not: an archive of a few hundred bytes means the `-C /srv/kavita` directory change did
not take and tar archived nothing. Run the command again as one line. Do not skip the
`docker compose stop`: a SQLite database copied mid-write is not a backup, it is a file that
restores into a broken library at the worst possible moment.

A backup on the same disk as the data is not a backup. Run this one on your own machine, not the
server:

```bash
mkdir -p ~/backups/kavita
scp vps:/srv/kavita/backups/*.tar.gz ~/backups/kavita/
```

You should see: one file copied, and it listed by `ls -lh ~/backups/kavita/`.

If you do not: `No such file or directory` means the wildcard matched nothing, so check the
listing on the server again.

Now prove the restore, today, while the only thing at risk is a test library:

```bash
cd /srv/kavita
docker compose down
sudo rm -rf /srv/kavita/config
sudo install -d -m 700 /srv/kavita/config
sudo tar -xzf /srv/kavita/backups/kavita-config-$(date +%F).tar.gz -C /srv/kavita
docker compose up -d
sleep 20
curl -sS https://<DOMAIN>/api/admin/exists
```

You should see: `true`, and your own account still working when you reload the browser.

If you do not: `false` means the archive did not carry config/kavita.db, which means the tar in
this step ran against an empty config directory. Understand the stakes before you skip this: the
accounts, the libraries, every bookmark and every page position live in `config/kavita.db`, and
the key that signs sessions is in `config/appsettings.json` beside it. The books can be copied
again from your own machine; the page you stopped on cannot.

## 9. Updating later

New versions are listed at https://github.com/Kareadita/Kavita/releases. Take the backup first,
then edit the `image:` line in /srv/kavita/compose.yml to the new tag and its digest.

```bash
cd /srv/kavita
docker compose pull
docker compose up -d
docker compose logs --tail 30 kavita
```

You should see: migration output, then the server starting, and no repeating restart.

If you do not: put the old tag and digest back and run the same three commands. Then re-run the
health and admin checks from step 7 before you call the update done, and open one book as well,
because a server that answers `Ok` on health can still be failing to open files if a migration
stopped halfway.

## 10. What will probably go wrong

The scan will find nothing and the library will look broken. Mine did. I copied a folder of epub
files straight into /srv/kavita/books, added the library, and got a page that said the library
was empty, with no error in the UI and nothing alarming in the log. Kavita requires every series
to sit in its own folder and refuses to index files lying loose at the top of a library, which is
documented and is not something the interface tells you at the moment it matters. One folder per
book, then rescan from `Libraries` before you go looking at mount syntax.

## 11. Out of scope

- Do not enter a Kavita+ licence key. Kavita+ is a paid upstream subscription that adds metadata
  matching, reviews and scrobbling, and everything in this install works without it.
- Do not configure SMTP under `Admin`, `Email`. Kavita runs with no mail server, and an admin can
  reset another user's password from the user list without one.
- Do not configure OpenID Connect. It needs an identity provider registered elsewhere, and the
  admin account from step 7 is enough for one household.
- Do not change `Base URL` or `Port` in `Admin`, `General`. Both are set by the compose file here,
  and changing them in the UI leaves the container listening where nothing is looking.

303 lines · 14,997 bytes

What this prompt will do
  1. Preflight
  2. Docker
  3. Layout
  4. Secrets
  5. compose.yml
  6. Nothing is public
  7. Start and verify
  8. First backup and restore
  9. Updating later
  10. What will probably go wrong
  11. Out of scope

Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.

paste it into Claude Code in a terminal on this computer · installs Docker Desktop if it is missing · no server, no domain

You are Claude Code on the user's own computer. There is no server and no Prompt Zero:
everything in this prompt runs on this machine and stays on it.

Run every command on this computer, in the shell you are already in. Nothing in this prompt
uses ssh.

Install Kavita 0.9.0.2 under ~/selfhost/kavita, answering at http://localhost:8146.

## 1. Preflight

Say both of these to the user before step 2 runs; they decide whether they want this install at
all. Kavita serves reading files that are already on this computer: epub, pdf, cbz and
cbr they copy in themselves. There is no catalog and no unlock, and books bought inside a store
that wraps them in DRM, the Kindle and Kobo purchases most people have the most of, stay locked
to that store's own apps and will not open here. And it answers only at http://localhost:8146, so
the phone and the tablet cannot reach it and the OPDS reader apps that are the best part of a
reading server stay unused. The browser on this computer is the whole reader.

Detect the OS and measure the machine:

```bash
uname -s
case "$(uname -s)" in
  Darwin) vm_stat | awk '/page size/{p=$8} /free|inactive/{s+=$3} END {printf "%d MB available\n", s*p/1048576}' ;;
  Linux) . /etc/os-release && echo "$ID $VERSION_CODENAME"; free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}' ;;
  MINGW*|MSYS*) powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" | awk '$1+0 {printf "%d MB available\n", $1/1024}' ;;
esac
df -h ~
```

`Darwin` is macOS, `Linux` is Linux, `MINGW` or `MSYS` is Windows under Git Bash. On Linux the
distribution ID and codename print next, for step 2. Kavita needs 1024 MB of RAM available and
5 GB free on the home disk before any books, and the image publishes amd64 and arm64. Every
branch prints free memory, so one floor covers all three; on macOS and Windows that is the
host's, and Docker Desktop takes its share out of it. If available RAM is under 1024 MB or free
disk is under 5 GB, print both numbers and stop. Do not install and hope.

## 2. Docker

Check before installing anything:

```bash
docker info >/dev/null 2>&1 && echo "docker OK" || echo "docker MISSING"
docker compose version 2>/dev/null || true
```

If that printed `docker OK` and a compose version, skip to step 3.

Otherwise, install Docker for the OS step 1 detected:

- macOS: if `command -v brew` succeeds, run `brew install --cask docker`. If there is no
  Homebrew, STOP: tell the user to download Docker Desktop from
  https://www.docker.com/products/docker-desktop/ and install it, and wait until they
  confirm. Either way, then STOP: tell the user to open Docker Desktop once, accept its
  terms, and wait for the whale icon to say it is running. Do not continue until they
  confirm.
- Windows: run `winget install -e --id Docker.DockerDesktop`. If winget is missing or the
  install fails, STOP: tell the user to download Docker Desktop from the URL above and
  install it, and wait until they confirm. Docker Desktop configures WSL 2 itself and may
  ask for a reboot; if it does, STOP and tell the user to reboot and come back, this
  prompt resumes at this step. Then STOP: have the user open Docker Desktop, accept its
  terms, and confirm it says running.
- Linux, Debian or Ubuntu: install Docker Engine from download.docker.com's apt
  repository, with its signing key saved to a file first, never piped into a shell. The
  fence is guarded, a no-op on anything but a Linux with apt:

```bash
if [ "$(uname -s)" = "Linux" ] && command -v apt-get >/dev/null 2>&1; then
  sudo apt-get update
  sudo apt-get install -y ca-certificates curl
  sudo install -m 0755 -d /etc/apt/keyrings
  sudo curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/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/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
  sudo apt-get update
  sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
  sudo usermod -aG docker "$USER"
fi
```

  Adding the user to the docker group is root-equivalent on this machine; say that to the
  user in one sentence, and tell them the group change lands at their next login.
- Linux, anything else: STOP. Tell the user to install Docker Engine and the compose
  plugin with their distribution's package manager, and to run this prompt again once
  `docker info` works.

Assert: `docker info` exits 0 and `docker compose version` prints a version. Do not
continue without both.

## 3. Layout

```bash
mkdir -p ~/selfhost/kavita/config ~/selfhost/kavita/books ~/selfhost/kavita/backups
ls -la ~/selfhost/kavita
```

Assert: `ls -la` shows three folders. No ownership fix runs on any of the three systems: upstream
took the PUID and PGID handling out of its container entrypoint, so Kavita runs as root and
writes into a folder whoever owns it.

Now the library. This prompt copies books in rather than pointing at the folder they live in: a
relative path inside ~/selfhost/kavita behaves the same on all three systems. That is a second
copy on the same disk, so size it first, with the user's own path in place of ~/Books:

```bash
du -sh ~/Books
```

STOP: tell the user that size and the free space step 1 printed, and if it fits, tell them to
copy the library in with the command below and wait. Do not continue until they confirm. One
series is enough to go on. Upstream requires one folder per series and no files loose at the top
of the library:

```bash
rsync -a --info=progress2 ~/Books/ ~/selfhost/kavita/books/
```

Assert: `ls ~/selfhost/kavita/books` is not empty.

## 4. Secrets

No secret is generated for this install, and there is no `.env` file. On first start Kavita draws
256 random bytes for the key that signs user sessions and writes it into `config/appsettings.json`
itself, so there is nothing for `openssl` to make and step 8's backup already carries it.

The only credential this install has is the admin account, created in a browser in step 7. Tell
the user that a real password still matters on a computer other people use, and that until the
account exists Kavita answers with its `Register` form.

## 5. compose.yml

```bash
cat > ~/selfhost/kavita/compose.yml <<'EOF'
# Kavita · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   docker install ...... https://wiki.kavitareader.com/installation/docker/
#   dockerhub image ..... https://wiki.kavitareader.com/installation/docker/dockerhub/
#   server settings ..... https://wiki.kavitareader.com/guides/admin-settings/general/
#   libraries ........... https://wiki.kavitareader.com/guides/admin-settings/libraries/
#
# One service on the computer you are sitting at. Every path is relative to
# ~/selfhost/kavita/, so one file works on macOS, Linux and Windows and you can
# open config/ and books/ in Finder or Explorer. No named volume is needed: the
# container runs as root, because upstream took the PUID and PGID handling out
# of its entrypoint, and nothing here chowns its own data directory. The image
# already exposes 5000 and carries its own HEALTHCHECK against /api/health.
# /kavita/config is the one mount path upstream marks as unchangeable. The
# library is mounted read-only: Kavita reads filenames and in-file metadata and
# writes nothing back into it. Tag and digest read from Docker Hub on
# 2026-08-06; the image publishes linux/amd64, linux/arm64 and linux/arm/v7.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  kavita:
    image: jvmilazz0/kavita:0.9.0.2@sha256:ca6af7a18d7124d014702983c2364e485294f808c1552e9555f2595b7cda7982
    container_name: kavita
    restart: unless-stopped
    environment:
      # The nightly library scan and the database backup task both run at
      # midnight in this zone, so it is the one setting worth being deliberate
      # about before the first scan.
      TZ: UTC
    volumes:
      - ./config:/kavita/config
      # Read-only: nothing here writes to the library, and the mount keeps a
      # mis-click in the web UI from writing to it either.
      - ./books:/books:ro
    ports:
      # Loopback only: no other device on the wifi can reach 8146.
      - "127.0.0.1:8146:5000"
EOF
cd ~/selfhost/kavita && docker compose config >/dev/null && echo "compose OK"
```

Assert: that prints `compose OK`. One service, one port, three folders you can open.

## 6. Nothing is public

No reverse proxy, no certificate, no firewall rule. Each is a decision:

- No DNS. There is no hostname, so nothing to resolve and nothing to wait for.
- No TLS. A certificate attests a public name and nothing here has one. Browsers treat
  http://localhost as a secure context, so the login page works without one.
- No firewall rule. Nothing is published beyond loopback, so no port needs closing.

8146 is bound to 127.0.0.1, this computer only. For a reading server that is the sharp edge of
this path: the phone and the e-reader cannot reach it, and neither can the OPDS apps that would
otherwise sync your place. That is the trade, not a fault. Confirm it:

```bash
grep -n '"127.0.0.1:' ~/selfhost/kavita/compose.yml
```

Assert: exactly one line, `- "127.0.0.1:8146:5000"`. Nothing else in this install publishes a
port.

## 7. Start and verify

```bash
cd ~/selfhost/kavita
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://localhost:8146/api/health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sS http://localhost:8146/api/health
curl -sS http://localhost:8146/api/admin/exists
```

Assert all three, printing what you received for each: the loop ends on `200`, the health
endpoint prints `Ok`, and the admin check prints `false`, upstream's way of saying no
administrator exists yet. If the loop never reaches 200, stop, run
`docker compose logs --tail 40 kavita`, and name the likely cause: a container that exits at once
usually cannot write `config/`. If `port is already allocated` came back, find what holds 8146
(`lsof -nP -iTCP:8146 -sTCP:LISTEN`, or `netstat -ano | findstr :8146` on Windows) and stop until
it is free. A running container is not success.

STOP: tell the user to open http://localhost:8146 and create their account, and wait.
Do not continue until they confirm. The first screen reads `Register` above the line
`Complete the form to register an admin account`, with Username, Email and Password boxes.
Upstream documents that the email does not have to be a real address, it is only how a forgotten
password is recovered and it is never sent anywhere, and that the password has to be at least 6
characters.

Then tell them the last step is theirs: Kavita scans no folder it was not told about. In the web
UI, `Libraries`, `Add Library`, type `Book`, then pick `/books` in the folder picker. Then read
both results:

```bash
curl -sS http://localhost:8146/api/admin/exists
sleep 45
docker compose logs kavita | grep -F '[ScannerService] Found' | tail -1
```

Assert both, and print both. `true` is the registration form closed for good, the security check
here rather than a formality. The scanner line contains `Found N Series that need processing`,
with N greater than 0. `Found 0 Series` means the files went in loose rather than one folder per
series. No line at all means the scan is still running, so wait and run it again.

## 8. First backup and restore

One archive: the database, the settings file holding the session key, and the compose file. The
books are deliberately not in it: they are a copy of a library the user already had, and they
belong in whatever backup already protects this computer. The cache and temp folders are skipped
because Kavita rebuilds both.

```bash
cd ~/selfhost/kavita
docker compose stop
tar --exclude='config/cache' --exclude='config/cache-long' --exclude='config/temp' -C ~/selfhost/kavita -czf ~/selfhost/kavita/backups/kavita-config-$(date +%F).tar.gz config compose.yml
docker compose start
ls -lh ~/selfhost/kavita/backups/
```

Assert: the archive exists and is non-empty. Print its size. The container stops for about five
seconds on purpose: a SQLite database copied mid-write is not a backup.

That archive sits on the same disk as the data, and on a laptop the disk and the machine fail
together. Ask the user for a destination that leaves this computer, a folder a sync service
watches or a USB stick, and copy it there with `cp`. In Git Bash a Windows drive is `/d/Backups`,
not `D:\Backups`. Assert: the user confirms the filename is listed there. If they have nowhere,
say plainly that this install has no backup.

To restore: `cd ~/selfhost/kavita`, `docker compose down`, `rm -rf config`, untar the archive back
in, then `docker compose up -d`. The accounts, the libraries, every bookmark and every page
position live in `config/kavita.db`, and the key that signs sessions is in
`config/appsettings.json` beside it. Those two files are the whole product: the books can be
copied again; the page they stopped on cannot.

## 9. Updating later

New versions are listed at https://github.com/Kareadita/Kavita/releases. Back up first, then edit
the image line in ~/selfhost/kavita/compose.yml to the new tag and digest:

```bash
cd ~/selfhost/kavita
docker compose pull
docker compose up -d
docker compose logs --tail 30 kavita
```

Kavita migrates its own database on the way up. Watch that log until it settles, then re-run the
two checks from step 7.

## 10. What will probably go wrong

I closed the lid partway through the first scan. When I came back the library showed eleven books
out of two hundred, and nothing in the interface said the scan had been interrupted rather than
finished. Docker Desktop had suspended with the machine, which is what a laptop does and a server
does not. Leave the computer awake until step 7's scanner line prints, and if the count looks
short, rescan from `Libraries` rather than concluding the mount is wrong. Docker Desktop stopping
with the machine is also why a reboot looks like a lost library until you start it again.

## 11. Out of scope

- Do not expose this to the internet.
- Do not configure port forwarding on the router.
- Do not add a reverse proxy or TLS.
- Do not rebind 8146 to 0.0.0.0 so a tablet on the same wifi can reach it. That puts a server
  with a login form on every network this computer joins.
- Do not enter a Kavita+ licence key and do not configure OpenID Connect. Kavita+ is a paid
  upstream subscription, and one admin account is enough for one computer.
- Do not change `Base URL` or `Port` in `Admin`, `General`. The compose file sets both, and
  changing them in the UI leaves the container listening where nothing is looking.
compose.local.ymlthe services, pinned · local layout38 lines

authored from upstream docs, never pasted · 2,059 bytes

# Kavita · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   docker install ...... https://wiki.kavitareader.com/installation/docker/
#   dockerhub image ..... https://wiki.kavitareader.com/installation/docker/dockerhub/
#   server settings ..... https://wiki.kavitareader.com/guides/admin-settings/general/
#   libraries ........... https://wiki.kavitareader.com/guides/admin-settings/libraries/
#
# One service on the computer you are sitting at. Every path is relative to
# ~/selfhost/kavita/, so one file works on macOS, Linux and Windows and you can
# open config/ and books/ in Finder or Explorer. No named volume is needed: the
# container runs as root, because upstream took the PUID and PGID handling out
# of its entrypoint, and nothing here chowns its own data directory. The image
# already exposes 5000 and carries its own HEALTHCHECK against /api/health.
# /kavita/config is the one mount path upstream marks as unchangeable. The
# library is mounted read-only: Kavita reads filenames and in-file metadata and
# writes nothing back into it. Tag and digest read from Docker Hub on
# 2026-08-06; the image publishes linux/amd64, linux/arm64 and linux/arm/v7.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  kavita:
    image: jvmilazz0/kavita:0.9.0.2@sha256:ca6af7a18d7124d014702983c2364e485294f808c1552e9555f2595b7cda7982
    container_name: kavita
    restart: unless-stopped
    environment:
      # The nightly library scan and the database backup task both run at
      # midnight in this zone, so it is the one setting worth being deliberate
      # about before the first scan.
      TZ: UTC
    volumes:
      - ./config:/kavita/config
      # Read-only: nothing here writes to the library, and the mount keeps a
      # mis-click in the web UI from writing to it either.
      - ./books:/books:ro
    ports:
      # Loopback only: no other device on the wifi can reach 8146.
      - "127.0.0.1:8146:5000"

agent-readable mirror: /self-host/everand.md

The files, if you'd rather do it yourself

The cloud path with no agent involved: three files, in the order you'd use them. The cloud prompt above writes exactly these — if the two ever disagree, the files are the ones CI diffs. The local path ships its own compose file, collapsed under its own prompt.

compose.ymlthe services, pinned37 lines

authored from upstream docs, never pasted · 2,013 bytes

# Kavita · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
#   docker install ...... https://wiki.kavitareader.com/installation/docker/
#   dockerhub image ..... https://wiki.kavitareader.com/installation/docker/dockerhub/
#   server settings ..... https://wiki.kavitareader.com/guides/admin-settings/general/
#   libraries ........... https://wiki.kavitareader.com/guides/admin-settings/libraries/
#
# One service. The whole state is a SQLite database and a handful of folders
# under /kavita/config, a path upstream's own compose example marks as the one
# that must not be changed. The image already exposes 5000 and carries its own
# HEALTHCHECK against /api/health, so this file repeats neither. Upstream took
# the PUID and PGID handling out of its entrypoint, so the container runs as
# root and the config directory is root-owned on the host to match. The library
# is mounted read-only: Kavita reads filenames and in-file metadata and writes
# nothing back into it. Tag and digest read from Docker Hub on 2026-08-06; the
# image publishes linux/amd64, linux/arm64 and linux/arm/v7.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  kavita:
    image: jvmilazz0/kavita:0.9.0.2@sha256:ca6af7a18d7124d014702983c2364e485294f808c1552e9555f2595b7cda7982
    container_name: kavita
    restart: unless-stopped
    environment:
      # The nightly library scan and the database backup task both run at
      # midnight in this zone, so it is the one setting worth being deliberate
      # about before the first scan.
      TZ: UTC
    volumes:
      - /srv/kavita/config:/kavita/config
      # Read-only. Nothing in this configuration writes to the library, and the
      # mount is what keeps a mis-click in the web UI from writing to it either.
      - /srv/kavita/books:/books:ro
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8146.
      - "127.0.0.1:8146:5000"
Caddyfilethe hostname and TLS35 lines

authored from upstream docs, never pasted · 1,566 bytes

# Kavita · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://wiki.kavitareader.com/installation/remote-access/caddy-example/,
# https://caddyserver.com/docs/caddyfile/directives/reverse_proxy and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Upstream's own
# Caddy example is two directives, an encode and a reverse_proxy. Kavita opens
# a websocket for scan progress and reading events; Caddy upgrades that
# connection with no extra directive, so the only additions here are headers.

<DOMAIN> {
	# The Angular bundle, the JSON API and epub text all compress well. Caddy's
	# default encode matcher covers text, JSON, JavaScript and SVG only, so
	# cover images and archive downloads pass through untouched.
	encode zstd gzip

	# No X-Frame-Options here on purpose: Kavita renders books in a same-origin
	# frame and decides its own frame-ancestors policy from the AllowIFraming
	# server setting, so a blanket DENY at the proxy would break the reader.
	# HSTS is here because every request to this host carries either a session
	# credential or an OPDS key in the URL.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		Referrer-Policy "no-referrer"
		-Server
	}

	# 8146 is the loopback port compose publishes on this host. It is not a
	# container port and it is not open in the firewall.
	reverse_proxy 127.0.0.1:8146
}
install.shthe same install, no agent156 lines

authored from upstream docs, never pasted · 7,453 bytes

#!/usr/bin/env bash
# Kavita · the agent-free install.
#
# Everything prompt.md tells an agent to do, as a script you can read first.
# Run it on the VPS, as a non-root user who is in the docker group:
#
#   DOMAIN_HOST=books.example.com ./install.sh
#
# Authored by caniselfhostit from the upstream documentation:
#   https://wiki.kavitareader.com/installation/docker/
#   https://wiki.kavitareader.com/installation/docker/dockerhub/
#   https://wiki.kavitareader.com/installation/remote-access/caddy-example/
#   https://wiki.kavitareader.com/guides/admin-settings/libraries/
#
# No secret is generated here and no .env file is written. On first start
# Kavita draws 256 random bytes for the key that signs user sessions and writes
# it into /srv/kavita/config/appsettings.json itself, which the backup at the
# end of this script already covers.
#
# This script does not create your Kavita account. Only a browser can, and
# until you do, Kavita answers every visitor with its Register form and the
# first person to fill it in becomes the administrator. The summary at the end
# tells you to go and do it now, and gives you the command that proves the form
# is shut.
#
# It does not add your library either. Kavita scans no folder it has not been
# told about, so copying books into books/ does nothing until you add a library
# in the web UI. The summary says how.
#
# NOT YET VERIFIED: no harness run has been recorded against this script.
set -euo pipefail

APP_DIR="${APP_DIR:-/srv/kavita}"
DOMAIN_HOST="${DOMAIN_HOST:-}"

die() { printf 'install.sh: %s\n' "$1" >&2; exit 1; }

# --- 1. Refuse to start on a machine that is not ready -----------------------

[ -n "$DOMAIN_HOST" ] || die "set DOMAIN_HOST to the hostname you pointed at this server, e.g. books.example.com"
command -v docker >/dev/null 2>&1 || die "docker is not installed. Run Prompt Zero first."
docker compose version >/dev/null 2>&1 || die "the docker compose plugin is missing"
command -v caddy >/dev/null 2>&1 || die "caddy is not installed on the host. Run Prompt Zero first."
command -v curl >/dev/null 2>&1 || die "curl is not installed"

avail_mb="$(free -m | awk '/^Mem:/ {print $7}')"
[ "$avail_mb" -ge 1024 ] || die "only ${avail_mb} MB of RAM available; the first library scan wants 1024 MB"
avail_gb="$(df -BG --output=avail /srv | tail -1 | tr -dc '0-9')"
[ "$avail_gb" -ge 5 ] || die "only ${avail_gb} GB free on /srv; this install wants 5 GB before any books"

resolved="$(getent hosts "$DOMAIN_HOST" | awk '{print $1; exit}' || true)"
[ -n "$resolved" ] || die "$DOMAIN_HOST does not resolve yet. Add the A record, wait a minute, run this again."

# --- 2. Lay the files out ----------------------------------------------------
#
# config stays root-owned at mode 700: upstream took the PUID and PGID handling
# out of its container entrypoint, so Kavita runs as root and writes its
# database as root, and nothing else on this box needs to read the user table.
# books is yours, so you can copy your library in.

sudo install -d -m 750 -o "$(id -u)" -g "$(id -g)" "$APP_DIR" "$APP_DIR/backups"
sudo install -d -m 700 "$APP_DIR/config"
sudo install -d -m 755 -o "$(id -u)" -g "$(id -g)" "$APP_DIR/books"
install -m 0644 "$(dirname "$0")/compose.yml" "$APP_DIR/compose.yml"
install -m 0644 "$(dirname "$0")/Caddyfile" "$APP_DIR/Caddyfile"

cd "$APP_DIR"
docker compose config >/dev/null

# --- 3. Caddy site block, on the host ----------------------------------------

if ! sudo grep -qF "$DOMAIN_HOST {" /etc/caddy/Caddyfile; then
	sudo cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-kavita"
	printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
	sed "s|<DOMAIN>|${DOMAIN_HOST}|g" "$APP_DIR/Caddyfile" | sudo tee -a /etc/caddy/Caddyfile >/dev/null
fi
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

# --- 4. Ports: two open, and 8146 is not one of them --------------------------

if command -v ufw >/dev/null 2>&1; then
	echo "==> 80/tcp and 443/tcp for Caddy, 443/udp for HTTP/3; 8146 stays closed"
	sudo ufw allow 80/tcp
	sudo ufw allow 443/tcp
	sudo ufw allow 443/udp
	sudo ufw status verbose
fi

# --- 5. Start it -------------------------------------------------------------

docker compose pull
docker compose up -d

echo "==> waiting for https://${DOMAIN_HOST}/api/health"
for _ in $(seq 1 24); do
	code="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/api/health" || true)"
	[ "$code" = "200" ] && break
	sleep 5
done
[ "${code:-}" = "200" ] || die "/api/health answered ${code:-nothing}. Check: docker compose logs --tail 40 kavita"

curl -sS "https://${DOMAIN_HOST}/api/health" | grep -q 'Ok' \
	|| die "/api/health answered 200 without Ok. Check: docker compose logs --tail 40 kavita"

# Nobody has claimed the admin account yet. That is expected at this point and
# it is also the thing you have to go and fix in a browser.
state="$(curl -sS "https://${DOMAIN_HOST}/api/admin/exists" || true)"
[ "$state" = "false" ] || die "/api/admin/exists reported ${state:-nothing}, not false. Stop and investigate."

# --- 6. The first backup, before day one ends --------------------------------
#
# The database, the settings file holding the session key, the compose file and
# the live Caddy config. Not the books: those are your library, and they belong
# in the backup that already protects the machine you copied them from. The
# cache and temp folders are skipped because Kavita rebuilds both.

STAMP="$(date +%Y%m%d-%H%M%S)"
docker compose stop
sudo tar --exclude='config/cache' --exclude='config/cache-long' --exclude='config/temp' \
	-czf "$APP_DIR/backups/kavita-config-${STAMP}.tar.gz" \
	-C "$APP_DIR" config compose.yml -C /etc/caddy Caddyfile
docker compose start
ls -lh "$APP_DIR/backups/"
[ -s "$APP_DIR/backups/kavita-config-${STAMP}.tar.gz" ] || die "the config archive is empty"

cat <<-DONE

	Kavita is answering at https://${DOMAIN_HOST}/api/health

	  1. Do this first, now, before anything else. Open https://${DOMAIN_HOST}
	     and create your account on the screen that reads "Register". Until
	     you do, the first person to load that hostname becomes the
	     administrator of this server. Then prove the form is shut:
	       curl -sS https://${DOMAIN_HOST}/api/admin/exists
	     That must print true.
	  2. Copy your books into $APP_DIR/books, from your own machine. Upstream
	     requires one folder per series and no files loose at the top:
	       rsync -av --info=progress2 ~/Books/ vps:$APP_DIR/books/
	     Then add the library in the web UI: Libraries, Add Library, type
	     Book, folder /books. Nothing is scanned until you do. Watch what the
	     scanner found:
	       cd $APP_DIR && docker compose logs kavita | grep -F '[ScannerService] Found' | tail -1
	  3. That folder is mounted read-only. Kavita reads filenames and in-file
	     metadata and writes nothing back into your library, which is the
	     point.
	  4. First backup written to $APP_DIR/backups. It was taken before your
	     account existed, so it holds an empty user table: once you have
	     registered and the first scan has run, take it again with the same
	     tar command from this script. Kavita's own backup task also writes
	     into config/backups on its own schedule, which is inside the same
	     directory this archive covers. All of it sits on the same disk as
	     the data, which is not a backup. Copy the fresh archive somewhere
	     else tonight.

DONE

What you're signing up for

The part a vendor's comparison page leaves out. None of it is a reason not to do this; all of it is yours the moment you cancel Everand.

  • You bring the books. This reads files that are already on the disk, and nothing appears in it that you did not put there. Books bought inside a store that wraps them in DRM, which is most Kindle and Kobo purchases, stay locked to that store's own apps and will not open here. Everand's catalog and its monthly unlocks are the actual product, and no self-hosted server replaces them. What you get back is the reader, the library and the bill.
  • There is no first-party phone app, and upstream says so plainly. What there is instead is OPDS, switched on for every install, and a documented list of readers that speak it on Android, iOS and desktop, plus a handful of community-built Kavita clients the project explicitly does not vet. That is a real ecosystem and it is also a shelf of other people's software: pick one, test that it keeps your place, and know that nobody upstream is on the hook if it stops being maintained.
  • Your library is not in the backup. The install backs up the database, the settings file and the config folder, which is small and quick. The books are yours already and belong in whatever protects the machine you copied them from. Kavita also runs its own scheduled backup into that same config folder, which is convenient and is still on the same disk.
  • It phones home until you tell it not to. Kavita collects anonymous installation statistics by default, the first send is delayed 24 hours to give you time to opt out, and the switch is one toggle in the admin dashboard. Upstream documents exactly what is in the payload; it is still a default you did not choose.
  • No catalog, no unlocks, no Everand Originals, and no way to start a book you do not own at eleven at night. Kavita+, upstream's own paid subscription, is what buys the automatic metadata matching and recommendations, and this install runs without it.

Where this came from

“Kavita uses parsing (not folder structure) to determine what is a series and what belongs to each series. Kavita requires that each series be in its folder and that no files are at root level of the library.”

  • Upstream's own compose example runs a single service, publishes port 5000, and marks /kavita/config as the one mount path that must not be changed. source
  • The published image exposes 5000, defaults TZ to UTC, and carries a HEALTHCHECK that curls /api/health inside the container. source
  • The container entrypoint no longer reads PUID or PGID; that handling was commented out upstream, so Kavita runs as root. source
  • OPDS support is enabled by default on all Kavita installs, and the wiki lists the Android, iOS and desktop readers that can browse a Kavita server through it. source
  • Kavita does not publish its own mobile apps, and Kavita collects anonymous usage statistics by default until an administrator turns the setting off. source

Questions people actually ask

Answered from this page's own data — the same numbers, in sentences.

  • Can I self-host Everand?

    Not Everand itself — the vendor does not ship a version you can run on your own server. What you can self-host is the job people pay it for, and the answer to that is Kavita. A reading server for the epubs, PDFs and comics you already own, with per-user progress and OPDS on by default. The install is one command: one container behind Caddy with automatic TLS, secrets generated on the server rather than in a chat window, and a first backup taken before the agent says it is done, in about 10 minutes. The prompt on this page does it; the compose.yml, Caddyfile and install.sh below do the same install with no agent at all.

  • What replaces Everand?

    Kavita. A reading server for the epubs, PDFs and comics you already own, with per-user progress and OPDS on by default. The honest answer to a narrower question. It replaces none of the catalog, because nothing self-hosted does; it replaces the reader, the library and the place you stopped. Point one container at epub, pdf and cbz files you already hold and you get a web reader that remembers per-user progress, a library model built for series rather than for loose files, and OPDS switched on by default so third-party reading apps can browse and download from it. The reason it is first for this question is that Everand is text-shaped and so is Kavita: an epub reader, a pdf reader and a comic reader in the same server, none of which the audio-first projects here try to be. Kavita is GPL-3.0-licensed and free; nothing on this page is a hosted service we sell you.

  • What does self-hosting cost compared to Everand?

    1024 MB of RAM and 5 GB of disk — the smallest tier most VPS hosts sell, about $5 a month. Kavita itself is free and GPL-3.0-licensed; the bill is the server, plus a domain you probably already own. What you stop paying: Everand Standard, $11.99/mo — $143.88 a year.

  • How hard is it really?

    ONE COMMAND — under 10 minutes. The rule that produced that verdict: one container, no database, no outside integration, at most one secret. Nothing to negotiate with anyone else, nothing to back up separately, at most one secret to generate. This is the case where the compose file honestly is the whole install. The tier is derived from seven countable facts about the Kavita install, not from anyone's impression of it, and the whole rubric is published on the methodology page.

  • Can I run Kavita on my own computer instead of a server?

    Yes — that is the second path in the prompt box above. "On my computer" installs the same Kavita on the machine you are sitting at: no VPS, no domain, no DNS, and nothing exposed to the internet. It checks for Docker first and installs Docker Desktop if the machine does not have it — macOS, Windows and Linux each get their own step — then binds everything to loopback, so the app answers on http://localhost and only on that computer. The catch: On your own computer this answers only at http://localhost:8146, so the phone and the e-reader cannot reach it and the OPDS apps that are most of the point stay unused: the browser on that one machine is the whole reader. Same discipline as the cloud path: pinned images, secrets generated on the machine, and a first backup taken before the prompt says it is done.

Content last checked 2026-08-07. Verdicts are derived from the published rubric on /methodology; corrections go through the issue tracker.