- Rust 50.7%
- JavaScript 39.1%
- HTML 8%
- Python 1.6%
- Shell 0.3%
- Other 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .github/workflows | ||
| docker | ||
| locales | ||
| migrations | ||
| scripts | ||
| service | ||
| src | ||
| static | ||
| templates | ||
| tests | ||
| .dockerignore | ||
| .gitignore | ||
| BENCHMARK.md | ||
| build.rs | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| config.toml.example | ||
| COVERAGE_REPORT.md | ||
| LICENSE | ||
| README.md | ||
| README_RU.md | ||
| THIRD-PARTY-LICENSES.md | ||
ROPDS
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.
What you get
- 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.
Quick start
1. Build
cargo build --release
2. Configure
cp config.toml.example config.toml
Minimum 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"
3. Create admin user
./target/release/ropds --set-admin <password>
4. Run
./target/release/ropds
Open:
- Web UI:
http://localhost:8081/web - OPDS:
http://localhost:8081/opds
One-shot scan
Scan the library once without starting the server:
./target/release/ropds --scan
Running with Docker
Pre-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.
Features
Library management
- 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)
OPDS catalog
- 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)
Search
- 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
Bookshelf
- 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
Book upload
- 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
Genres
- 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
User management
- 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
OAuth and access requests
- 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
Embedded book reader
- 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
Web interface
- 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
Internationalization
- Ships with English, Russian, German, and Simplified Chinese 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 — English, Russian, and Simplified Chinese names ship out of the box
- Per-user language preference saved in a cookie
Adding a new locale
- 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.
Security
- Argon2 password hashing
- HMAC-SHA256 signed session cookies
- Configurable session lifetime
- Per-user upload permissions
- Superuser role for admin access
Configuration
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, de, zh), 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 |
OAuth login and approval
- 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 = true
Deployment
Systemd
Use 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 -f
Docker
Ready-to-run bundle with compose files for SQLite, PostgreSQL, and MySQL/MariaDB:
- English guide:
docker/README.md - Russian guide:
docker/README_RU.md
Reverse proxy
Nginx and Traefik snippets:
- English:
service/proxy/README.md - Russian:
service/proxy/README_RU.md
Supported formats
| 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.
Database
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://).
Migrating between backends (SQLite -> PostgreSQL or MySQL/MariaDB)
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
Tech stack
| 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 |
Performance
Apache Bench results: BENCHMARK.md (~29K req/s, ~135K req/s with keep-alive).
License
Dual-licensed under MIT or Apache-2.0, at your option.