Self-hosted e-book library server with OPDS, web reader and Telegram bot (.NET)
620
Source code, issues and docs: jncchds/nopds
A self-hosted e-book library server: scan your book folders, browse and read them in a modern web app, and serve them to e-readers over OPDS.
Built with ASP.NET Core (.NET 10), PostgreSQL and a React + TypeScript single-page app, served together from one process.
Inspired by SimpleOPDS (sopds) by Dmitry Shelepnev. The OPDS URL layout is kept compatible, so existing reader bookmarks keep working.
Library
Reading & downloads
ebook-convert, kepubify) that chain with them (DOCX → EPUB → AZW3), with an LRU cacheClients
/opds/t/<token>/) for apps without login supportWeb app
Security
git clone https://github.com/jncchds/nopds.git
cd nopds
NOPDS_BOOKS=/path/to/your/books NOPDS_ADMIN_PASSWORD=choose-a-password docker compose up -d --build
Open http://localhost:8080, sign in as admin, go to Administration → Libraries → Add library and choose /books (the folder mounted from NOPDS_BOOKS). Start a scan.
The compose file runs two containers:
| Service | Purpose |
|---|---|
nopds | Web app, API, OPDS, scanner, bot. Data (keys, logs, cache) in the nopds-data volume |
postgres | PostgreSQL 17, data in the nopds-db volume |
Mount more folders (read-only) in docker-compose.yml to add more libraries.
| Client | URL |
|---|---|
| OPDS 1.2 with login | http://<host>:8080/opds/ |
| OPDS 1.2 personal link | http://<host>:8080/opds/t/<token>/ (see Settings, also as QR code) |
| OPDS 2.0 | http://<host>:8080/opds/v2/ or …/opds/t/<token>/v2/ |
| One library only | add l/<libraryId>/, e.g. /opds/l/2/ |
| KOReader progress sync | Custom sync server http://<host>:8080/kosync, your user name and the sync password set in Settings |
Any OPDS 1.2 client should work (KOReader, Moon+ Reader, FBReader, Librera, …); OPDS 2.0 is supported by newer apps such as Thorium and Readest.
Host settings come from appsettings.json or environment variables (__ separates sections):
| Setting | Default | Description |
|---|---|---|
ConnectionStrings__Nopds | local PostgreSQL | Npgsql connection string |
Nopds__DataDir | data | Signing keys, data-protection keys, logs |
Nopds__CacheDir | data/cache | Covers, thumbnails, converted books |
Nopds__AdminUser / Nopds__AdminPassword | admin / – | First admin, created only when no users exist |
Nopds__AdminForce | false | Recovery: on every start, create AdminUser if missing, reset its password to AdminPassword, restore admin rights, approve and unlock it |
Nopds__AutoMigrate | true | Apply database migrations on start |
Nopds__Jwt__Key | generated | HMAC key (≥ 32 bytes); generated into DataDir when empty |
Nopds__Jwt__AccessTokenMinutes | 15 | Access token lifetime |
Nopds__Jwt__RefreshTokenDays | 30 | Refresh token lifetime |
Nopds__Oidc__Authority | – | OpenID Connect issuer URL; single sign-on is off when empty |
Nopds__Oidc__ClientId / Nopds__Oidc__ClientSecret | – | OAuth2 client credentials |
Nopds__Oidc__DisplayName | Authentik | Provider name on the login button |
Nopds__Oidc__Scopes__0… | openid profile email | Requested scopes |
Nopds__UploadPath | – | Folder of the upload library; uploading is off when empty (see below) |
Nopds__UploadMaxMegabytes | 200 | Largest accepted upload (all files of one upload together) |
Everything else (site title, public/private access, page sizes, duplicate handling, covers, converters, Telegram bot, SSO approval) is edited at runtime in Administration → Settings. Library options (extensions, ZIP code page, INPX, schedule, watching, soft delete, hashing) are per library.
Behind a reverse proxy, forward X-Forwarded-Proto and X-Forwarded-Host so OPDS links use the public address.
Set Nopds__UploadPath to let users add books themselves. On start a library for that folder is created (named Uploads) and scanned, tracked and browsed like any other; every signed-in user can open it and upload to it from the Upload page, whatever their per-library access.
Uploads are public by default. The uploader can mark a book private when uploading or later (upload page or book page); private books are shown only to the uploader and admins — in the web app, OPDS feeds, downloads and the Telegram bot. Admins can change the privacy of any upload.
Files placed in the folder by other means (copied in by hand, or there before uploads were enabled) are treated as public uploads of the first user (the oldest account, normally the main admin) on the next scan.
When a scan finds an uploaded file missing, the book is hidden, not removed, so its owner and privacy come back with the file.
The folder must be writable. In Docker, bind it to a host folder so books survive container rebuilds — uncomment both lines in docker-compose.yml:
environment:
Nopds__UploadPath: "/uploads"
volumes:
- ${NOPDS_UPLOADS:-./uploads}:/uploads
The container runs as UID 1654, so give it the host folder: mkdir -p uploads && sudo chown 1654 uploads.
Removing Nopds__UploadPath turns uploading off; the library stays as a regular one and private books stay private.
openid, profile, email) with the redirect URI https://<your-nopds-host>/signin-oidc, and an Application using it (slug e.g. nopds).Nopds__Oidc__Authority: "https://auth.example.com/application/o/nopds/"
Nopds__Oidc__ClientId: "<client id>"
Nopds__Oidc__ClientSecret: "<client secret>"
preferred_username; a number is appended if a local user already has that name — existing accounts are never taken over). By default the account waits until an admin approves it in Administration → Users, where admins also grant or revoke admin rights; turn this off in Administration → Settings → Single sign-on.NOPDS must be served over HTTPS for the sign-in round trip (the OIDC correlation cookies are Secure). E-readers cannot use SSO: SSO users open their personal feed link from Settings, or set a local password there for HTTP Basic auth.
Requirements: .NET 10 SDK, Node.js 22, Docker.
docker compose -f docker-compose.dev.yml up -d # PostgreSQL on localhost:5432
dotnet tool restore # dotnet-ef
dotnet run --project src/Nopds.Web # API on http://localhost:5249 (admin / admin123 in Development)
cd src/Nopds.Web/ClientApp && npm install && npm run dev # SPA on http://localhost:5173 (proxies to the API)
Useful commands:
dotnet test # unit + integration tests (Testcontainers PostgreSQL)
cd src/Nopds.Web/ClientApp && npm run lint && npm run lint:i18n && npm run build
dotnet ef migrations add <Name> -p src/Nopds.Infrastructure -s src/Nopds.Infrastructure -o Data/Migrations
dotnet publish src/Nopds.Web -c Release # also builds the SPA into wwwroot
src/Nopds.Domain Entities and text helpers (language codes, transliteration, normalization)
src/Nopds.Infrastructure EF Core/PostgreSQL, migrations, Identity user, settings, genre catalog, catalog queries
src/Nopds.Formats FB2/EPUB/MOBI/CBZ parsers, book storage, covers, hashing
src/Nopds.Scanner Incremental scanner, INPX reader, scheduler, folder watcher
src/Nopds.Conversion EPUB 3 converters (FB2, DOCX, ODT, RTF, TXT, HTML), external converters, routing, cache
src/Nopds.Opds OPDS 1.2 Atom and OPDS 2.0 JSON writers
src/Nopds.Telegram Telegram bot
src/Nopds.Web ASP.NET Core host: API, auth, OPDS/kosync endpoints, SignalR; ClientApp/ = React SPA
tests/Nopds.Tests Parser, converter and integration tests
docs/ Notes on the original SimpleOPDS architecture
GPL-3.0. This project ports logic and data (language codes, transliteration, INPX parsing, the FB2 genre list) from SimpleOPDS, which is licensed under GPLv3.
Content type
Image
Digest
sha256:64c2b9f68…
Size
106.3 MB
Last updated
6 days ago
docker pull jncchds/nopds