Appearance
Production Deployment
Container Image
A single unified image is published to the Forgejo container registry via manual workflow dispatch. It contains both the Hono API server and the Vue 3 + Vite 8 SPA frontend — Hono serves the static files directly.
| Image | Port | Description |
|---|---|---|
registry.example.com/user/libris | 3000 | Hono API + SPA (unified) |
Tags:
v<api-version>-web<web-version>— composite tag from both package versions (e.g.,v0.17.1-web2.10.1). Either version changing produces a new tag. Bumped via changesets — runpnpm changesetto describe a change, and the Publish Images workflow runschangeset versionto bump.latest— most recent build
Building the image
Trigger the Publish Images workflow from the Forgejo UI: Actions > Publish Images > Run workflow. It builds the unified image from Dockerfile at the current HEAD.
Pulling the image
bash
docker login registry.example.com
docker pull registry.example.com/user/libris:latestDeployment Model
The SPA and API are served from the same origin — no CORS configuration needed. The auth cookie works automatically since both are on the same host.
books.example.com/ → SPA (served by Hono)
books.example.com/api/* → Hono API
books.example.com/opds/* → Hono API (e-reader catalog)
books.example.com/kosync/* → Hono API (reading progress sync)
books.example.com/_docs/* → Hono API (OpenAPI docs)Environment Variables
Required
| Variable | Purpose |
|---|---|
POSTGRES_HOST | Postgres host. The app assembles the connection URL from the POSTGRES_* split vars. |
POSTGRES_PORT | Postgres port. Optional, defaults to 5432. |
POSTGRES_USER | Postgres user. |
POSTGRES_PASSWORD | Postgres password. |
POSTGRES_DB | Postgres database name. |
REDIS_HOST | Redis host. The app assembles the connection URL from the REDIS_* split vars. |
REDIS_PORT | Redis port. Optional, defaults to 6379. |
REDIS_USER | Redis ACL user. Optional. |
REDIS_PASSWORD | Redis password. Optional. |
REDIS_TLS | Set to 1 for rediss:// (TLS). Required by most managed Redis providers. |
LIBRIS_INBOX_PATH | Writable directory for uploaded book files |
LIBRIS_LIBRARY_PATH | Writable directory for organized book storage |
API_SECRET_KEY | Token/cookie encryption secret — minimum 32 characters |
Optional
| Variable | Purpose |
|---|---|
PORT | Port the API server listens on. Default: 3000. |
COOKIE_DOMAIN | Parent domain for auth cookie (e.g., .example.com). Leave empty for same-origin. |
MIGRATIONS_PATH | Path to migration files directory. Default: ./migrations. |
TRUST_PROXY_HEADERS | Set to 1 behind a trusted reverse proxy so X-Real-IP / X-Forwarded-For drive auth logging and rate limiting. Default: 0. See Reverse Proxy below. |
LOG_LEVEL | Pino and OTel log level: trace, debug, info, warn, error, fatal. Default: info. |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP collector URL (e.g., http://alloy:4318). Setting this is what activates the OTel SDK. |
OTEL_SERVICE_NAME | Service name in telemetry data. Default: libris. |
OTEL_EXPORTER_OTLP_PROTOCOL | OTLP protocol: http/protobuf, http/json, or grpc. Default: http/protobuf. |
OTEL_TRACES_EXPORTER | Trace exporter. Default: otlp. Set to none to disable traces. |
OTEL_LOGS_EXPORTER | Log exporter. Default: otlp. Set to none to disable OTel log export (stdout Pino output is unaffected). |
OTEL_METRICS_EXPORTER | Metrics exporter. Default: otlp. Set to none to disable metrics. |
Volumes
The container needs persistent, writable storage for two paths:
| Mount target | Purpose |
|---|---|
Value of LIBRIS_INBOX_PATH | Incoming book files (watched for ingestion) |
Value of LIBRIS_LIBRARY_PATH | Organized book library |
Example Docker Compose
yaml
services:
db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_USER: libris
POSTGRES_PASSWORD: changeme
POSTGRES_DB: libris
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U libris"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
libris:
image: registry.example.com/user/libris:latest
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
environment:
POSTGRES_HOST: db
POSTGRES_PORT: "5432"
POSTGRES_USER: libris
POSTGRES_PASSWORD: changeme
POSTGRES_DB: libris
REDIS_HOST: redis
REDIS_PORT: "6379"
LIBRIS_INBOX_PATH: /data/inbox
LIBRIS_LIBRARY_PATH: /data/library
API_SECRET_KEY: # openssl rand -hex 32
volumes:
- inbox:/data/inbox
- library:/data/library
ports:
- "3000:3000"
volumes:
db_data:
inbox:
library:Reverse Proxy
By default, Libris ignores X-Forwarded-For and X-Real-IP for auth logging and rate limiting and uses the real TCP peer address instead. When running behind a trusted reverse proxy (nginx, Caddy, Traefik), set TRUST_PROXY_HEADERS=1 and configure the proxy to pass the client IP headers:
nginx:
nginx
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;Without this, repeated auth failures from the same client may not be correctly rate-limited.
Database Migrations
Migrations apply automatically on API startup — runMigrations() in services/api-hono/src/bootstrap.ts runs before the server starts accepting requests. No manual migration step is needed when deploying a new image. The migrations directory is resolved from MIGRATIONS_PATH (default ./migrations, which the Dockerfile copies into the image alongside the bundled server).