Fast, lightweight self-hosted e-book library server with an OPDS 1.2 / 2.0 catalog and a web UI. Built in Rust.
Inspired by SimpleOPDS, rebuilt from scratch as a modern alternative for home servers and small VPS instances.
- Single Rust binary, no runtime dependencies, containers optional
- OPDS 1.2 / 2.0 feeds compatible with popular readers (CoolReader, FBReader, Librera, KOReader, etc.)
- Web UI with browsing, search, admin panel, book uploads, and a built-in reader
- Multi-user accounts with per-user upload permissions
- OAuth sign-in (Google, Yandex, Keycloak OIDC) with approval queue and optional email notifications
- Background library scanning including ZIP archives and INPX index files
- Light and dark themes, installable as a PWA
The goal is a personal library server you set up once and forget about — easy to deploy for yourself, family, and friends.
This project doubles as an educational pet project exploring the modern Rust ecosystem, with heavy use of competitive LLM coding agents throughout development.
cargo build --releasecp config.toml.example config.tomlMinimum required settings:
[server]
base_url = "http://localhost:8081"
[library]
root_path = "/path/to/books"Full example with covers and database:
[server]
base_url = "http://localhost:8081"
[library]
root_path = "/path/to/books"
[covers]
covers_path = "/path/to/books/covers"
cover_max_dimension_px = 600
cover_jpeg_quality = 85
show_covers = true
[database]
url = "sqlite://ropds.db?mode=rwc"./target/release/ropds --set-admin <password>./target/release/ropdsOpen:
- Web UI:
http://localhost:8081/web - OPDS:
http://localhost:8081/opds
Scan the library once without starting the server:
./target/release/ropds --scanPre-built multi-architecture images (linux/amd64, linux/arm64) are published on every release:
- GitHub Container Registry:
ghcr.io/dshein-alt/ropds - Docker Hub:
docker.io/dsheinalt/ropds
See docker/README.md for the full deployment guide, including a no-source-checkout quick start.
- Background scanning on a configurable cron schedule
- Parallel scanning with worker-limited dynamic task scheduling
- Books inside ZIP archives and INPX index files are handled transparently
- Metadata extraction for FB2, EPUB, and MOBI — title, authors, genres, series, covers, annotations
- Optional cover generation for PDF and DjVu via external tools (
pdftoppm,ddjvu)
- Full OPDS 1.2 / 2.0 feeds with pagination
- Browse by author, series, genre, catalog, or title prefix
- OpenSearch support
- Cover thumbnails and full-size images
- HTTP Basic Auth (can be disabled)
- Full-text search across titles, authors, and series — from both OPDS and the web UI
- Alphabetical prefix browsing with configurable split threshold for large collections (default matches the prefix at any word boundary; set
opds.alphabet_first_word_only = trueto restrict matches to the first word of each name) - OpenSearch descriptor for OPDS client integration
- Personal reading list per user — add or remove books with one click
- Books are automatically added to the bookshelf on download
- Sort by date added, title, or author in either direction
- Infinite scroll
- Upload books directly through the web interface (FB2, EPUB, PDF, and other supported formats)
- Metadata is extracted automatically with immediate editing — adjust title, authors, and genres before saving
- Per-user upload permissions controlled by the admin
- Hierarchical genre system with sections and subcategories
- Per-language genre translations stored in the database
- Admin UI for creating sections, adding genres, and managing translations
- Flexible tagging — multiple genres per book, editable at any time
- Multi-user support with a built-in admin panel
- Create and delete users, reset passwords, toggle upload permissions
- Users manage their own profile: display name and password
- OAuth users can regenerate a dedicated OPDS password from their profile
- Forced password change on first login when set by admin
- Providers: Google, Yandex, Keycloak (OIDC)
- New OAuth users enter a pending state until an administrator approves them
- Admins can approve, reject, ban, or reinstate access requests
- Approval supports linking an OAuth identity to an existing local account
- Keycloak: optional auto-approval and role-based mapping for upload and admin permissions
- Optional SMTP notifications to admin on new and re-applied requests
- Read EPUB, FB2, MOBI, DjVu, and PDF directly in the browser — no downloads required
- Automatic reading position save and restore per user per book
- Reading history sidebar with quick access to recently read books
- Opens in a new tab in browsers and in the same window when installed as a PWA
- Powered by foliate-js and djvu.js
- Responsive Bootstrap 5 UI with light and dark themes
- Installable as a PWA on mobile and desktop (manifest + service worker)
- Browse by catalog, author, series, or genre with breadcrumb navigation
- Inline book metadata editing for admins (title, authors, genres)
- Duplicates page: duplicate editions grouped by title + authors, with pagination
- Cover preview with full-size overlay on click
- Ships with English and Russian locales
- Locale files are plain TOML in
locales/— the file stem is the locale code (en.toml→en) - Genre names support per-language translations in the database
- Per-user language preference saved in a cookie
- Copy
locales/en.tomltolocales/<code>.toml(e.g.de.toml) and translate every value. - In the new file's
[lang]section, add an entry for the new code with its native name (e.g.de = "Deutsch"). This label is used by the OPDS language facet. - Add the same
<code> = "<Native name>"entry to the[lang]section of every other locale file so the web language selector shows a translated label in each UI language. - Rebuild — locales are discovered automatically from the
locales/directory (filesystem in debug, embedded in release). No source-code changes required.
- Argon2 password hashing
- HMAC-SHA256 signed session cookies
- Configurable session lifetime
- Per-user upload permissions
- Superuser role for admin access
All settings live in config.toml. See config.toml.example for a fully commented reference.
server.base_url is required — it is used for OAuth callback URLs and links in admin notification emails.
| Section | Key highlights |
|---|---|
[server] |
Bind address, port, log level, session secret, TTL, base_url |
[library] |
Book root path, file extensions, ZIP/INPX support |
[covers] |
covers_path, resize and compression (cover_max_dimension_px, cover_jpeg_quality), show_covers |
[database] |
Connection URL — sqlite://, postgres://, or mysql:// |
[opds] |
Catalog title, pagination, auth, alphabet drill-down mode (alphabet_first_word_only) |
[scanner] |
Cron schedule, parallel workers, integrity checks |
[web] |
Default language (en, ru), default theme (light, dark) |
[upload] |
Enable/disable uploads, staging directory, size limit |
[reader] |
Enable/disable embedded reader, reading history size |
[oauth] |
Provider credentials, moderation settings, Keycloak role mapping, notification toggle |
[smtp] |
SMTP server settings for outbound email notifications |
- Set
server.base_urlto your externally reachable URL. - Configure at least one provider in
[oauth](google_*,yandex_*, or Keycloak settings). - (Optional) Enable admin notifications: set
oauth.notify_admin_email = trueand fill in[smtp]. - Users sign in via
/web/login. - New users land in Admin -> Access Requests until approved.
Minimal example (Google + admin email notifications):
[server]
base_url = "https://books.example.com"
[oauth]
google_client_id = "..."
google_client_secret = "..."
notify_admin_email = true
[smtp]
host = "smtp.example.com"
port = 587
username = "smtp-user"
password = "smtp-pass"
from = "ropds@example.com"
send_to = ["admin@example.com", "alerts@example.com"]
starttls = trueUse the template unit file from service/ropds.unit (runs under the ropds user account).
sudo useradd --system --home /opt/ropds --shell /usr/sbin/nologin ropds || true
sudo install -d -o ropds -g ropds /opt/ropds
sudo install -m 0755 target/release/ropds /opt/ropds/ropds
sudo install -m 0644 config.toml /opt/ropds/config.toml
sudo install -m 0644 service/ropds.unit /etc/systemd/system/ropds.service
sudo systemctl daemon-reload
sudo systemctl enable --now ropds.service
sudo systemctl status ropds.service
sudo journalctl -u ropds.service -fReady-to-run bundle with compose files for SQLite, PostgreSQL, and MySQL/MariaDB:
- English guide:
docker/README.md - Russian guide:
docker/README_RU.md
Nginx and Traefik snippets:
- English:
service/proxy/README.md - Russian:
service/proxy/README_RU.md
| Format | Metadata | Covers |
|---|---|---|
| FB2 | Full (title, authors, genres, series, annotation, language) | Embedded |
| EPUB | Full (OPF metadata) | Embedded |
| MOBI | Full (title, author, description, language, date) | Embedded |
Limited (title, author via pdfinfo) |
First page (via pdftoppm) |
|
| DjVu | Filename only | First page (via ddjvu) |
Books inside ZIP archives are scanned transparently. INPX index files are supported as an alternative to scanning individual archives.
SQLite is the default and simplest option — no setup needed. PostgreSQL and MySQL/MariaDB are also supported via [database].url.
Supported versions (tested in CI and verified end-to-end):
| Backend | Minimum | Notes |
|---|---|---|
| SQLite | 3.35+ | Bundled via sqlx; no install needed. |
| PostgreSQL | 16+ | Tested on 16 and 17. Earlier PG versions may work but are not exercised. |
| MariaDB | 11+ | Tested on 11.x and 12.x. |
| MySQL | 8+ | Tested on 8.4. Requires default ONLY_FULL_GROUP_BY SQL mode to work. |
For SQLite, a scanner parallelism setting of workers_num = 2..4 is usually the sweet spot. Higher values can increase write-lock contention during large rescans.
Migrations run automatically on startup. Backend-specific migration sets are embedded at build time and selected by the database URL prefix (sqlite://, postgres://, mysql://).
Four-step flow:
- Create the role and database on the target (one-time). The role only needs to own the DB — no superuser or
rootrequired. - Prepare the target schema with
ropds --init-db— creates the database if missing, applies every migration, clears every user table so the target is truly empty, and exits. Refuses if the target already has rows (so it is safe to invoke accidentally against a live or already-migrated DB — you'll be told to reset it manually). - Copy the data with
scripts/migrate_sqlite.py— minimal helper that only does truncate + copy + verify in a single CLI session. Precheck: every target data table must have 0 rows (the state--init-dbleaves behind); otherwise the script lists the offenders and refuses. Requires interactive confirmation; depends only onpsqlormysql/mariadb. Supports running those clients inside a container via--db-container NAME --container-runtime {docker,podman}. - Start ROPDS against the new URL.
- English step-by-step guide (with SQL examples for PG and MySQL/MariaDB):
scripts/README.md - Russian step-by-step guide:
scripts/README_RU.md
| Component | Choice |
|---|---|
| Language | Rust (edition 2024) |
| Web framework | Axum 0.8 |
| Async runtime | Tokio |
| Database | SQLx (SQLite / PostgreSQL / MySQL) |
| Templates | Tera |
| UI | Bootstrap 5 + Bootstrap Icons |
| Password hashing | Argon2 |
| XML parsing | quick-xml |
| Parallelism | Tokio task queue + DashMap |
Apache Bench results: BENCHMARK.md (~29K req/s, ~135K req/s with keep-alive).
Dual-licensed under MIT or Apache-2.0, at your option.