Skip to content

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.

ImagePortDescription
registry.example.com/user/libris3000Hono 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 — run pnpm changeset to describe a change, and the Publish Images workflow runs changeset version to 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:latest

Deployment 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

VariablePurpose
POSTGRES_HOSTPostgres host. The app assembles the connection URL from the POSTGRES_* split vars.
POSTGRES_PORTPostgres port. Optional, defaults to 5432.
POSTGRES_USERPostgres user.
POSTGRES_PASSWORDPostgres password.
POSTGRES_DBPostgres database name.
REDIS_HOSTRedis host. The app assembles the connection URL from the REDIS_* split vars.
REDIS_PORTRedis port. Optional, defaults to 6379.
REDIS_USERRedis ACL user. Optional.
REDIS_PASSWORDRedis password. Optional.
REDIS_TLSSet to 1 for rediss:// (TLS). Required by most managed Redis providers.
LIBRIS_INBOX_PATHWritable directory for uploaded book files
LIBRIS_LIBRARY_PATHWritable directory for organized book storage
API_SECRET_KEYToken/cookie encryption secret — minimum 32 characters

Optional

VariablePurpose
PORTPort the API server listens on. Default: 3000.
COOKIE_DOMAINParent domain for auth cookie (e.g., .example.com). Leave empty for same-origin.
MIGRATIONS_PATHPath to migration files directory. Default: ./migrations.
TRUST_PROXY_HEADERSSet 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_LEVELPino and OTel log level: trace, debug, info, warn, error, fatal. Default: info.
OTEL_EXPORTER_OTLP_ENDPOINTOTLP collector URL (e.g., http://alloy:4318). Setting this is what activates the OTel SDK.
OTEL_SERVICE_NAMEService name in telemetry data. Default: libris.
OTEL_EXPORTER_OTLP_PROTOCOLOTLP protocol: http/protobuf, http/json, or grpc. Default: http/protobuf.
OTEL_TRACES_EXPORTERTrace exporter. Default: otlp. Set to none to disable traces.
OTEL_LOGS_EXPORTERLog exporter. Default: otlp. Set to none to disable OTel log export (stdout Pino output is unaffected).
OTEL_METRICS_EXPORTERMetrics exporter. Default: otlp. Set to none to disable metrics.

Volumes

The container needs persistent, writable storage for two paths:

Mount targetPurpose
Value of LIBRIS_INBOX_PATHIncoming book files (watched for ingestion)
Value of LIBRIS_LIBRARY_PATHOrganized 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).