docs(deploy): add unified Docker Compose guide and update dev workflow

Introduce a comprehensive Docker Compose documentation page covering slim, full, and local-build stack examples. Add example Compose files under docker/examples/ for streamlined deployment. Update local development docs to reference the new Compose guide. Revise package.json scripts to support the new Compose workflows and remove legacy compute-worker compose commands. Update sidebar to include the Docker Compose documentation for improved discoverability.
This commit is contained in:
Richard R 2026-06-13 13:21:18 -06:00
parent 5403aa00cc
commit 2abadde949
9 changed files with 477 additions and 91 deletions

View file

@ -73,12 +73,14 @@ COPY --from=app-builder /app/src/db ./src/db
# Merge in the dependency subset needed by the entrypoint migration scripts.
COPY --from=app-builder /opt/entrypoint-migration-tools/node_modules /tmp/runtime-tools-node_modules
RUN mkdir -p /app/node_modules && \
rm -rf /app/node_modules/pg && \
rm -rf /tmp/runtime-tools-node_modules/@aws-sdk \
/tmp/runtime-tools-node_modules/better-sqlite3 \
/tmp/runtime-tools-node_modules/ffmpeg-static \
/tmp/runtime-tools-node_modules/pg && \
/tmp/runtime-tools-node_modules/ffmpeg-static && \
cp -an /tmp/runtime-tools-node_modules/. /app/node_modules/ && \
rm -rf /tmp/runtime-tools-node_modules
# Drizzle Kit discovers the Postgres driver through ESM import during startup migrations.
RUN node --input-type=module -e "await import('pg')"
# Ship the embedded compute worker as a separate deployed bundle.
COPY --from=app-builder /opt/embedded-compute-worker ./embedded-compute-worker

View file

@ -1,44 +0,0 @@
services:
nats:
image: nats:2.14-alpine
container_name: openreader-compute-nats
command: ["-js", "-sd", "/data"]
ports:
- "4222:4222"
- "8222:8222"
volumes:
- nats-data:/data
compute-worker:
build:
context: ..
dockerfile: compute-worker/Dockerfile
container_name: openreader-compute-worker
depends_on:
- nats
env_file:
- path: ./.env
required: false
environment:
NATS_URL: ${NATS_URL:-nats://nats:4222}
COMPUTE_WORKER_HOST: ${COMPUTE_WORKER_HOST:-0.0.0.0}
PORT: ${PORT:-8081}
COMPUTE_WORKER_TOKEN: ${COMPUTE_WORKER_TOKEN:-local-compute-token}
S3_PREFIX: ${S3_PREFIX:-openreader}
COMPUTE_PREWARM_MODELS: ${COMPUTE_PREWARM_MODELS:-false}
ports:
- "8081:8081"
develop:
watch:
- action: sync+restart
path: .
target: /workspace
- action: rebuild
path: ./package.json
- action: rebuild
path: ../pnpm-lock.yaml
- action: rebuild
path: ../pnpm-workspace.yaml
volumes:
nats-data:

View file

@ -0,0 +1,124 @@
name: openreader-example
services:
openreader:
image: ghcr.io/richardr1126/openreader:latest
# Keep localhost:8333 valid for both app requests and browser-facing presigned URLs.
network_mode: service:seaweedfs
depends_on:
kokoro-tts:
condition: service_started
postgres:
condition: service_healthy
seaweedfs:
condition: service_healthy
environment:
BASE_URL: ${BASE_URL:-http://localhost:3003}
AUTH_SECRET: ${AUTH_SECRET:-local-openreader-auth-secret-change-me}
POSTGRES_URL: postgres://openreader:openreader@postgres:5432/openreader
COMPUTE_WORKER_URL: http://compute-worker:8081
COMPUTE_WORKER_TOKEN: ${COMPUTE_WORKER_TOKEN:-local-compute-token}
USE_EMBEDDED_WEED_MINI: "false"
S3_ENDPOINT: ${S3_ENDPOINT:-http://localhost:8333} # Used for internal endpoint and public facing presigned URLs.
S3_BUCKET: ${S3_BUCKET:-openreader-documents}
S3_REGION: ${S3_REGION:-us-east-1}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-devkey}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-devsecret}
S3_FORCE_PATH_STYLE: "true"
S3_PREFIX: ${S3_PREFIX:-openreader}
RUNTIME_SEED_JSON: |-
{
"version": 1,
"runtimeConfig": {
"defaultTtsProvider": "kokoro"
},
"providers": [
{
"slug": "kokoro",
"displayName": "Kokoro",
"providerType": "custom-openai",
"baseUrl": "http://kokoro-tts:8880/v1",
"defaultModel": "kokoro",
"enabled": true
}
]
}
volumes:
- openreader-docstore:/app/docstore
kokoro-tts:
image: ghcr.io/remsky/kokoro-fastapi-cpu:v0.2.4
environment:
ONNX_NUM_THREADS: 8
ONNX_INTER_OP_THREADS: 4
ONNX_EXECUTION_MODE: parallel
ONNX_OPTIMIZATION_LEVEL: all
ONNX_MEMORY_PATTERN: "true"
ONNX_ARENA_EXTEND_STRATEGY: kNextPowerOfTwo
API_LOG_LEVEL: DEBUG
ports:
- "8880:8880"
seaweedfs:
image: chrislusf/seaweedfs:4.18
command: ["mini", "-dir=/data"]
environment:
AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-devkey}
AWS_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-devsecret}
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9333/cluster/status >/dev/null"]
interval: 2s
timeout: 2s
retries: 30
ports:
- "3003:3003"
- "8333:8333"
volumes:
- seaweedfs-data:/data
nats:
image: nats:2.14-alpine
command: ["-js", "-sd", "/data"]
volumes:
- nats-data:/data
postgres:
image: postgres:17-alpine
environment:
POSTGRES_USER: openreader
POSTGRES_PASSWORD: openreader
POSTGRES_DB: openreader
healthcheck:
test: ["CMD-SHELL", "pg_isready -U openreader -d openreader"]
interval: 2s
timeout: 2s
retries: 30
volumes:
- postgres-data:/var/lib/postgresql/data
compute-worker:
image: ghcr.io/richardr1126/openreader-compute-worker:latest
depends_on:
nats:
condition: service_started
seaweedfs:
condition: service_healthy
environment:
NATS_URL: nats://nats:4222
COMPUTE_WORKER_HOST: 0.0.0.0
PORT: 8081
COMPUTE_WORKER_TOKEN: ${COMPUTE_WORKER_TOKEN:-local-compute-token}
S3_ENDPOINT: http://seaweedfs:8333
S3_BUCKET: ${S3_BUCKET:-openreader-documents}
S3_REGION: ${S3_REGION:-us-east-1}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-devkey}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-devsecret}
S3_FORCE_PATH_STYLE: "true"
S3_PREFIX: ${S3_PREFIX:-openreader}
COMPUTE_PREWARM_MODELS: ${COMPUTE_PREWARM_MODELS:-false}
volumes:
openreader-docstore:
seaweedfs-data:
nats-data:
postgres-data:

View file

@ -0,0 +1,130 @@
name: openreader-local-build
services:
openreader:
image: openreader:local
build:
context: ../..
dockerfile: Dockerfile
# Keep localhost:8333 valid for both app requests and browser-facing presigned URLs.
network_mode: service:seaweedfs
depends_on:
kokoro-tts:
condition: service_started
postgres:
condition: service_healthy
seaweedfs:
condition: service_healthy
environment:
BASE_URL: ${BASE_URL:-http://localhost:3003}
AUTH_SECRET: ${AUTH_SECRET:-local-openreader-auth-secret-change-me}
POSTGRES_URL: postgres://openreader:openreader@postgres:5432/openreader
COMPUTE_WORKER_URL: http://compute-worker:8081
COMPUTE_WORKER_TOKEN: ${COMPUTE_WORKER_TOKEN:-local-compute-token}
USE_EMBEDDED_WEED_MINI: "false"
S3_ENDPOINT: ${S3_ENDPOINT:-http://localhost:8333} # Used for internal endpoint and public facing presigned URLs.
S3_BUCKET: ${S3_BUCKET:-openreader-documents}
S3_REGION: ${S3_REGION:-us-east-1}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-devkey}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-devsecret}
S3_FORCE_PATH_STYLE: "true"
S3_PREFIX: ${S3_PREFIX:-openreader}
RUNTIME_SEED_JSON: |-
{
"version": 1,
"runtimeConfig": {
"defaultTtsProvider": "kokoro"
},
"providers": [
{
"slug": "kokoro",
"displayName": "Kokoro",
"providerType": "custom-openai",
"baseUrl": "http://kokoro-tts:8880/v1",
"defaultModel": "kokoro",
"enabled": true
}
]
}
volumes:
- openreader-docstore:/app/docstore
kokoro-tts:
image: ghcr.io/remsky/kokoro-fastapi-cpu:v0.2.4
environment:
ONNX_NUM_THREADS: 8
ONNX_INTER_OP_THREADS: 4
ONNX_EXECUTION_MODE: parallel
ONNX_OPTIMIZATION_LEVEL: all
ONNX_MEMORY_PATTERN: "true"
ONNX_ARENA_EXTEND_STRATEGY: kNextPowerOfTwo
API_LOG_LEVEL: DEBUG
ports:
- "8880:8880"
seaweedfs:
image: chrislusf/seaweedfs:4.18
command: ["mini", "-dir=/data"]
environment:
AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-devkey}
AWS_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-devsecret}
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9333/cluster/status >/dev/null"]
interval: 2s
timeout: 2s
retries: 30
ports:
- "3003:3003"
- "8333:8333"
volumes:
- seaweedfs-data:/data
nats:
image: nats:2.14-alpine
command: ["-js", "-sd", "/data"]
volumes:
- nats-data:/data
postgres:
image: postgres:17-alpine
environment:
POSTGRES_USER: openreader
POSTGRES_PASSWORD: openreader
POSTGRES_DB: openreader
healthcheck:
test: ["CMD-SHELL", "pg_isready -U openreader -d openreader"]
interval: 2s
timeout: 2s
retries: 30
volumes:
- postgres-data:/var/lib/postgresql/data
compute-worker:
image: openreader-compute-worker:local
build:
context: ../..
dockerfile: compute-worker/Dockerfile
depends_on:
nats:
condition: service_started
seaweedfs:
condition: service_healthy
environment:
NATS_URL: nats://nats:4222
COMPUTE_WORKER_HOST: 0.0.0.0
PORT: 8081
COMPUTE_WORKER_TOKEN: ${COMPUTE_WORKER_TOKEN:-local-compute-token}
S3_ENDPOINT: http://seaweedfs:8333
S3_BUCKET: ${S3_BUCKET:-openreader-documents}
S3_REGION: ${S3_REGION:-us-east-1}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-devkey}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-devsecret}
S3_FORCE_PATH_STYLE: "true"
S3_PREFIX: ${S3_PREFIX:-openreader}
COMPUTE_PREWARM_MODELS: ${COMPUTE_PREWARM_MODELS:-false}
volumes:
openreader-docstore:
seaweedfs-data:
nats-data:
postgres-data:

View file

@ -0,0 +1,49 @@
name: openreader-slim-example
services:
openreader:
image: ghcr.io/richardr1126/openreader:latest
depends_on:
kokoro-tts:
condition: service_started
environment:
BASE_URL: ${BASE_URL:-http://localhost:3003}
AUTH_SECRET: ${AUTH_SECRET:-local-openreader-auth-secret-change-me}
RUNTIME_SEED_JSON: |-
{
"version": 1,
"runtimeConfig": {
"defaultTtsProvider": "kokoro"
},
"providers": [
{
"slug": "kokoro",
"displayName": "Kokoro",
"providerType": "custom-openai",
"baseUrl": "http://kokoro-tts:8880/v1",
"defaultModel": "kokoro",
"enabled": true
}
]
}
ports:
- "3003:3003"
- "8333:8333"
volumes:
- openreader-docstore:/app/docstore
kokoro-tts:
image: ghcr.io/remsky/kokoro-fastapi-cpu:v0.2.4
environment:
ONNX_NUM_THREADS: 8
ONNX_INTER_OP_THREADS: 4
ONNX_EXECUTION_MODE: parallel
ONNX_OPTIMIZATION_LEVEL: all
ONNX_MEMORY_PATTERN: "true"
ONNX_ARENA_EXTEND_STRATEGY: kNextPowerOfTwo
API_LOG_LEVEL: DEBUG
ports:
- "8880:8880"
volumes:
openreader-docstore:

View file

@ -0,0 +1,153 @@
---
title: Docker Compose
description: Run OpenReader with the slim, full, or local-build Docker Compose examples.
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
Use these examples to run OpenReader with Kokoro-FastAPI and persistent storage. Choose the slim
stack for the simplest deployment, or the full stack when you want PostgreSQL, SeaweedFS, NATS,
and the compute worker as separate containers.
## Prerequisites
- A recent Docker version with Docker Compose
- A clone of the OpenReader repository
```bash
git clone https://github.com/richardr1126/openreader.git
cd openreader
```
## Choose a stack
<Tabs groupId="docker-compose-stack">
<TabItem value="slim" label="Slim" default>
The default slim example runs:
- OpenReader with embedded SeaweedFS, NATS, compute worker, and SQLite
- Kokoro-FastAPI as a companion container
```bash
docker compose -f docker/examples/compose.yml up
# Repository convenience command: pnpm compose
```
Compose file: [`docker/examples/compose.yml`](https://github.com/richardr1126/openreader/blob/main/docker/examples/compose.yml)
</TabItem>
<TabItem value="full" label="Full">
The full example runs OpenReader, Kokoro-FastAPI, PostgreSQL, SeaweedFS, NATS, and the compute
worker as separate containers using published images.
```bash
docker compose -f docker/examples/compose.full.yml up
# Repository convenience command: pnpm compose:full
```
Compose file: [`docker/examples/compose.full.yml`](https://github.com/richardr1126/openreader/blob/main/docker/examples/compose.full.yml)
For details about running the worker separately, see
[Compute Worker](./compute-worker).
</TabItem>
<TabItem value="local" label="Local Build">
The local-build example uses the full multi-container layout, but builds the OpenReader app and
compute-worker images from the current checkout.
```bash
docker compose -f docker/examples/compose.local.yml up --build
# Repository convenience command: pnpm compose:local
```
Compose file: [`docker/examples/compose.local.yml`](https://github.com/richardr1126/openreader/blob/main/docker/examples/compose.local.yml)
</TabItem>
</Tabs>
## Included services
| Service | Slim | Full | Local Build |
| --- | --- | --- | --- |
| OpenReader | Published image | Published image | Local build |
| Kokoro-FastAPI | Container | Container | Container |
| Database | Embedded SQLite | PostgreSQL container | PostgreSQL container |
| SeaweedFS | Embedded | Container | Container |
| NATS | Embedded | Container | Container |
| Compute worker | Embedded | Published image | Local build |
On first boot, `RUNTIME_SEED_JSON` creates an enabled Kokoro shared provider and selects it as the
default TTS provider.
## Endpoints
- OpenReader: `http://localhost:3003`
- SeaweedFS S3: `http://localhost:8333`
- Kokoro-FastAPI: `http://localhost:8880`
In the full examples, PostgreSQL, the compute worker, and NATS remain internal to the Compose
network.
## LAN access
Set `BASE_URL` and `S3_ENDPOINT` to the Docker host's LAN IP so browser-facing app and presigned
S3 URLs are reachable from other devices:
<Tabs groupId="docker-compose-lan-stack">
<TabItem value="slim" label="Slim" default>
```bash
BASE_URL=http://192.168.0.XXX:3003 \
S3_ENDPOINT=http://192.168.0.XXX:8333 \
docker compose -f docker/examples/compose.yml up
# Repository convenience command: pnpm compose
```
</TabItem>
<TabItem value="full" label="Full">
```bash
BASE_URL=http://192.168.0.XXX:3003 \
S3_ENDPOINT=http://192.168.0.XXX:8333 \
docker compose -f docker/examples/compose.full.yml up
# Repository convenience command: pnpm compose:full
```
</TabItem>
<TabItem value="local" label="Local Build">
```bash
BASE_URL=http://192.168.0.XXX:3003 \
S3_ENDPOINT=http://192.168.0.XXX:8333 \
docker compose -f docker/examples/compose.local.yml up --build
# Repository convenience command: pnpm compose:local
```
</TabItem>
</Tabs>
Replace `192.168.0.XXX` with your Docker host's LAN IP and allow inbound TCP ports `3003` and
`8333` through its firewall.
:::info Internal full-stack endpoint
The full and local-build compute workers continue using `http://seaweedfs:8333` internally.
`S3_ENDPOINT` configures the app endpoint and browser-facing presigned URLs.
:::
## Configuration
The examples use local-only default credentials. Override existing `${VARIABLE}` values through
your shell environment before using them beyond local development.
:::warning Protect public deployments
Replace the default `AUTH_SECRET`, PostgreSQL credentials, S3 credentials, and compute-worker
token before exposing a stack outside your trusted local network.
:::
For the complete configuration reference, see
[Environment Variables](../reference/environment-variables). See [Database](../configure/database)
for PostgreSQL and SQLite behavior.

View file

@ -159,45 +159,10 @@ If you need mirrors or pinned artifact locations, set `WHISPER_MODEL_BASE_URL` i
</details>
<details>
<summary><strong>External compute worker dev stack (optional)</strong></summary>
Use this only when you intentionally run compute-worker as a separate service.
Default local flow does not need `compute-worker/.env`; embedded worker startup reads root `.env`.
Full worker deployment details are in [Compute Worker (NATS JetStream)](./compute-worker).
Start only NATS + compute-worker via compose watch:
```bash
docker compose --env-file compute-worker/.env -f compute-worker/docker-compose.yml up --watch
# or: pnpm compute:dev:compose
```
`compute-worker/.env.example` contains a starter config for standalone worker service deployments.
Run the main app separately on the host:
```bash
pnpm dev
```
For app -> external worker routing, set in root `.env`:
```env
COMPUTE_WORKER_URL=http://localhost:8081
COMPUTE_WORKER_TOKEN=<same-token-used-by-worker>
```
Ownership in external worker mode:
- root `.env`: app routing/auth (`COMPUTE_WORKER_URL`, `COMPUTE_WORKER_TOKEN`) plus optional shared timeout/stale/retry overrides such as `COMPUTE_PDF_JOB_ATTEMPTS`
- `compute-worker/.env*` (or worker platform env): worker runtime variables (`NATS_*`, `S3_*`, model base URLs, worker tuning)
For embedded worker startup (`COMPUTE_WORKER_URL` unset), worker tuning values such as `COMPUTE_PDF_JOB_ATTEMPTS` must be set in the root `.env` because `compute-worker/.env*` is ignored in that mode.
Worker mode requires worker-reachable shared object storage (S3-compatible endpoint).
For external worker mode, object storage must be shared/reachable by both app and worker services.
</details>
:::tip Docker Compose
To run OpenReader and Kokoro-FastAPI with Docker Compose, including slim, full, and local-build
options, see [Docker Compose](./docker-compose).
:::
## Steps

View file

@ -55,7 +55,12 @@ const sidebars: SidebarsConfig = {
{
type: 'category',
label: '🚀 Deploy',
items: ['deploy/local-development', 'deploy/compute-worker', 'deploy/vercel-deployment'],
items: [
'deploy/local-development',
'deploy/docker-compose',
'deploy/compute-worker',
'deploy/vercel-deployment',
],
},
{
type: 'category',

View file

@ -16,8 +16,9 @@
"test:e2e": "playwright test",
"test:unit": "vitest run",
"test:compute": "vitest run --project compute-worker",
"compute:openapi:generate": "pnpm --dir compute-worker openapi:generate && openapi-typescript compute-worker/openapi.json -o src/lib/server/compute-worker/generated.ts",
"compute:openapi:check": "pnpm compute:openapi:generate && git diff --exit-code -- compute-worker/openapi.json src/lib/server/compute-worker/generated.ts",
"compose": "docker compose -f docker/examples/compose.yml up",
"compose:full": "docker compose -f docker/examples/compose.full.yml up",
"compose:local": "docker compose -f docker/examples/compose.local.yml up --build",
"migrate": "node drizzle/scripts/migrate.mjs",
"migrate-fs": "node scripts/migrate-fs-v2.mjs",
"migrate-fs:dry-run": "node scripts/migrate-fs-v2.mjs --dry-run true",
@ -28,9 +29,10 @@
"docs:serve": "pnpm --dir docs-site serve",
"docs:version": "pnpm --dir docs-site docusaurus docs:version",
"docs:clear": "pnpm --dir docs-site clear",
"compute:worker:dev": "pnpm --filter @openreader/compute-worker dev",
"compute:worker:start": "pnpm --filter @openreader/compute-worker start",
"compute:dev:compose": "docker compose --env-file compute-worker/.env -f compute-worker/docker-compose.yml up --build",
"compute:dev": "pnpm --filter @openreader/compute-worker dev",
"compute:start": "pnpm --filter @openreader/compute-worker start",
"compute:openapi:generate": "pnpm --dir compute-worker openapi:generate && openapi-typescript compute-worker/openapi.json -o src/lib/server/compute-worker/generated.ts",
"compute:openapi:check": "pnpm compute:openapi:generate && git diff --exit-code -- compute-worker/openapi.json src/lib/server/compute-worker/generated.ts",
"lint:route-errors": "node scripts/check-route-error-responses.mjs"
},
"dependencies": {