- Go 71.8%
- JavaScript 22.7%
- HTML 3.6%
- CSS 1.7%
- Shell 0.1%
|
Some checks are pending
/ Lint (push) Has started running
/ Test (push) Has started running
/ E2E Browser (push) Has started running
/ JS Unit Tests (push) Successful in 1m17s
/ E2E API (push) Successful in 2m46s
/ Integration (push) Successful in 3m42s
/ Build and publish (amd64) (push) Successful in 3m48s
|
||
|---|---|---|
| .beads | ||
| .claude | ||
| .forgejo/workflows | ||
| cmd/pergamum | ||
| data | ||
| docker/mysql-ci | ||
| docs | ||
| e2e | ||
| internal | ||
| loadtest | ||
| scripts | ||
| static | ||
| templates | ||
| .dockerignore | ||
| .editorconfig | ||
| .gitignore | ||
| .gitmodules | ||
| .golangci.yml | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| dev.env.example | ||
| docker-compose.worker.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| Dockerfile.ci | ||
| Dockerfile.mysql-ci | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| sqlc.yaml | ||
| vitest.config.js | ||
Pergamum
A self-hosted digital library server for eBooks, comics, PDFs, and audiobooks. Grimmory-compatible schema rewrite in Go.
Local development
Copy dev.env.example to ~/.config/pergamum/dev.env, fill in your values (API keys, admin credentials, books directory, host uid:gid), then run make up. The file lives outside the repo so it is never committed. Every worktree's make up / make dev / make down reads it automatically when present.
Commands
pergamum [FLAGS] [SUBCOMMAND [FLAGS] ...]
With no subcommand, the HTTP server starts (equivalent to pergamum serve).
serve (default)
Start the HTTP server.
pergamum [FLAGS]
pergamum serve [FLAGS]
Both forms start the HTTP server. The bare form is provided for backward compatibility with existing scripts and docker-compose files.
scan
Synchronously scan a configured library and populate the database with books. Useful for initial seeding and development without needing background workers.
pergamum scan <library_id>
A library must be created via the HTTP API before scanning. Example sequence:
docker compose up -d # start server
# create the library
curl -X POST -H 'Content-Type: application/json' \
-d '{"name":"My Books","organization_mode":"BOOK_PER_FILE","allowed_formats":"epub,pdf"}' \
http://localhost:8080/libraries
# add a path to the library (use the library id returned above)
curl -X POST -H 'Content-Type: application/json' \
-d '{"path":"/srv/books"}' \
http://localhost:8080/libraries/1/paths
# scan the library
docker compose run --rm pergamum scan 1
All output is structured JSON (via log/slog). Progress logs are emitted every 100 files or 10 seconds, whichever comes first.
Example:
PERGAMUM_DSN="user:pass@tcp(localhost:3306)/bookshelf?parseTime=true" pergamum scan 1
worker
Run the background task pool as a standalone process (no HTTP server).
pergamum worker [FLAGS]
See Deployment — worker modes below for when to use this subcommand.
Configuration
Configuration is loaded from flags, PERGAMUM_* environment variables, and an optional config file (key=value lines). Precedence: flags > env > config file > built-in defaults.
Flags (shared by all subcommands):
--http-addr— HTTP server listen address (default:8080)--metrics-addr— Prometheus metrics listen address (default: same as--http-addr;pergamum workeruses this for/metrics,/healthz, and/readyz)--dsn— MySQL DSN--data-dir— root directory for derived data (covers, thumbnails)--log-level—debug,info,warn,error(defaultinfo)--log-format—jsonortext(defaultjson)--config— path to optional config file--slow-query-log— log queries slower than this threshold (default100ms,0disables)--worker-count— number of concurrent worker goroutines (default4)--worker-mode—embedded,external, ordisabled(defaultembedded; see below)
Deployment — worker modes
Pergamum supports three worker pool topologies selected by --worker-mode / PERGAMUM_WORKER_MODE.
embedded (default)
The HTTP server starts and owns the worker pool in the same process.
pergamum # or: PERGAMUM_WORKER_MODE=embedded pergamum
Suitable for single-process deployments and local development (docker compose up).
external
The HTTP server does not start a pool. A separate pergamum worker process handles task processing.
# Terminal 1 — HTTP server only
PERGAMUM_WORKER_MODE=external pergamum
# Terminal 2 — standalone worker pool
PERGAMUM_METRICS_ADDR=:9091 pergamum worker
Use the compose override to run this topology with Docker Compose:
docker compose -f docker-compose.yml -f docker-compose.worker.yml up
Note: After pulling changes that add database migrations, rebuild the Docker image to ensure the app's embedded migrations are up to date (docker compose ... up --build). Docker Compose reuses cached images by default; without a rebuild, the binary's embedded migrations may lag behind the database, resulting in "no migration found for version N" errors.
Scale worker replicas horizontally (each replica is a separate pool; they safely contend on the tasks table via the CAS-claim protocol):
docker compose -f docker-compose.yml -f docker-compose.worker.yml up --scale worker=N
disabled
Neither the HTTP server nor a separate pergamum worker process runs a pool. Use for read-only replicas or environments where task processing is handled elsewhere.
PERGAMUM_WORKER_MODE=disabled pergamum
Single-cron-owner rule
Cron always runs in the HTTP server process (embedded or external mode) — never in pergamum worker.
The cron scheduler is a singleton owned by the app process. In external mode, the app enqueues tasks on schedule and the worker process claims them. In disabled mode, no tasks are enqueued. Multiple pergamum worker replicas never race on cron because they do not run it.
Worker health endpoints
The standalone pergamum worker process exposes three endpoints on PERGAMUM_METRICS_ADDR:
| Endpoint | Purpose | Success |
|---|---|---|
/metrics |
Prometheus scrape target | 200 |
/healthz |
Liveness — process is alive and serving | 200 |
/readyz |
Readiness — DB is reachable; worker can claim tasks | 200 |
/readyz pings the database with a 3-second timeout and returns 503 Service Unavailable when the ping fails, allowing orchestrators to hold traffic until the worker is ready.
Deployment
See docs/releases.md for Docker image streams, self-hoster setup, release procedures, and versioning policy.
Help
pergamum --help
pergamum serve --help
pergamum scan --help
pergamum worker --help