A tiny, dependency-free OPDS ebook server with a drag-and-drop upload page. Drop EPUBs onto a web page, then browse and download them from any OPDS-capable e-reader (built with the XTEink in mind, but it's standard OPDS 1.2).
- One address for both: open
http://<server-ip>:8080in a browser for the upload page; e-readers that connect to the same address get the OPDS catalog (all books A-Z, recently added, search). The catalog is also always available at/opds - Reads title and author from EPUB metadata (falls back to the filename)
- Sort the book list by title, author, date added or size (your choice is remembered)
- Select several books (shift-click for a range, or select all) and delete them in one go
- Mark books as read on the web page (one at a time, or several at once) and filter by
Read/Unread. Read books get a
[Read]prefix in the OPDS catalog, so you can see them on your e-reader. The status is saved in.simpleopds.jsoninside your books folder, so it stays with your books and their backups - Supports EPUB, PDF, MOBI, AZW3, CBZ, TXT and FB2
- Python standard library only; small multi-arch image (amd64 and arm64)
- Optional basic auth; runs as a non-root user
services:
opds:
image: ghcr.io/hkrlogic/simpleopds:latest
container_name: opds
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- opds-books:/books
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
#environment:
# OPDS_USER: reader
# OPDS_PASS: change-me
# MAX_UPLOAD_MB: 500
volumes:
opds-books:docker compose up -d
In Portainer: Stacks → Add stack → Web editor → paste the above → Deploy.
To add a password (recommended if anyone else can reach the port): delete just
the # character at the start of the four environment lines, leave the spaces
alone, and change change-me to your own password. The lines are laid out so
this always gives valid YAML.
Then:
- Open
http://<server-ip>:8080/and drag your books onto the page. - On your e-reader, add a catalog:
http://<server-ip>:8080(if it can't connect, tryhttp://<server-ip>:8080/opds). If you set a username and password, enter them in the reader too.
| Variable | Default | Description |
|---|---|---|
OPDS_USER |
unset | Enables HTTP basic auth (with OPDS_PASS) everywhere |
OPDS_PASS |
unset | Password for basic auth |
MAX_UPLOAD_MB |
500 |
Largest allowed upload |
Books live in /books inside the container. The container always runs as an
unprivileged user (UID 1000) and never as root, so the folder you mount has to
be writable by that user:
- New named volume (as above): works automatically.
- Existing volume or bind mount (e.g.
/srv/books:/books): make it writable once withsudo chown -R 1000:1000 /srv/books. For an existing named volume in Portainer, open the container's Console asrootand runchown -R 1000:1000 /books, or delete the empty volume and redeploy. - Not sure? The container logs a warning at startup if the folder isn't writable, and the upload page shows the exact command to run.
- Need a different UID? Set
user: "<uid>:<gid>"in your compose file.
Without OPDS_USER/OPDS_PASS, anyone who can reach the port can upload and
delete books. That's fine on a trusted home LAN, but set a password before
exposing it any further. The server speaks plain HTTP: if you put it on the
internet, run it behind a reverse proxy that terminates TLS (it honors
X-Forwarded-Proto and X-Forwarded-Host so catalog links use https://).
Basic auth over plain HTTP sends the password unencrypted.
Only the listed ebook extensions can be uploaded, filenames are sanitized, downloads can't escape the books folder, and state-changing requests require a custom header so other websites can't trigger them from your browser.
The container never runs as root, and the example compose file drops every
Linux capability, blocks privilege escalation (no-new-privileges) and mounts
the root filesystem read-only. The app needs none of those privileges, so a bug
in it has very little to work with. If your Docker host doesn't support one of
these options, remove that line; the app doesn't depend on them.
python3 server.py --books ./books --port 8080
Requires Python 3.8 or newer.
git clone https://github.com/hkrlogic/simpleOPDS
cd simpleOPDS
docker build -t simpleopds .
