Add a second MinIO client (pub) initialized with MINIO_PUBLIC_ENDPOINT so presigned audio URLs are signed against the public hostname from the start, rather than signed internally and then rewritten. This avoids AWS4 signature mismatch (SignatureDoesNotMatch 403) that occurred when the signed host was substituted after signing. - storage/minio.go: add PublicEndpoint/PublicUseSSL to MinioConfig; add pub client field; NewMinioClient creates pub client when public endpoint differs; PresignAudio uses pub, PresignChapter keeps internal client - cmd/scraper/main.go: wire MINIO_PUBLIC_ENDPOINT and MINIO_PUBLIC_USE_SSL env vars - docker-compose.yml: expose MINIO_PUBLIC_ENDPOINT and MINIO_PUBLIC_USE_SSL to scraper service - ui/src/lib/server/minio.ts: remove rewriteHost() call from presignAudio
186 lines
7.2 KiB
YAML
186 lines
7.2 KiB
YAML
version: "3.9"
|
|
|
|
services:
|
|
# ─── MinIO (object storage for chapter .md files + audio cache) ─────────────
|
|
minio:
|
|
image: minio/minio:latest
|
|
#container_name: libnovel-minio
|
|
restart: unless-stopped
|
|
command: server /data --console-address ":9001"
|
|
environment:
|
|
MINIO_ROOT_USER: "${MINIO_ROOT_USER:-admin}"
|
|
MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:-changeme123}"
|
|
ports:
|
|
- "${MINIO_PORT:-9000}:9000" # S3 API
|
|
- "${MINIO_CONSOLE_PORT:-9001}:9001" # Web console
|
|
volumes:
|
|
- minio_data:/data
|
|
healthcheck:
|
|
test: ["CMD", "mc", "ready", "local"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
# ─── MinIO bucket initialisation ─────────────────────────────────────────────
|
|
# Runs once to create the default buckets and then exits.
|
|
minio-init:
|
|
image: minio/mc:latest
|
|
#container_name: libnovel-minio-init
|
|
depends_on:
|
|
minio:
|
|
condition: service_healthy
|
|
entrypoint: >
|
|
/bin/sh -c "
|
|
mc alias set local http://minio:9000 $${MINIO_ROOT_USER:-admin} $${MINIO_ROOT_PASSWORD:-changeme123};
|
|
mc mb --ignore-existing local/libnovel-chapters;
|
|
mc mb --ignore-existing local/libnovel-audio;
|
|
echo 'buckets ready';
|
|
"
|
|
environment:
|
|
MINIO_ROOT_USER: "${MINIO_ROOT_USER:-admin}"
|
|
MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:-changeme123}"
|
|
|
|
# ─── PocketBase (auth + structured data: books, chapters index, ranking, progress) ──
|
|
pocketbase:
|
|
image: ghcr.io/muchobien/pocketbase:latest
|
|
#container_name: libnovel-pocketbase
|
|
restart: unless-stopped
|
|
environment:
|
|
# Auto-create superuser on first boot (used by entrypoint.sh)
|
|
PB_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL:-admin@libnovel.local}"
|
|
PB_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD:-changeme123}"
|
|
ports:
|
|
- "${POCKETBASE_PORT:-8090}:8090"
|
|
volumes:
|
|
- pb_data:/pb_data
|
|
healthcheck:
|
|
test: ["CMD", "wget", "-qO-", "http://localhost:8090/api/health"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
# ─── PocketBase collection bootstrap ────────────────────────────────────────
|
|
# One-shot init container: creates all required collections via the admin API
|
|
# and exits. Idempotent — safe to run on every `docker compose up`.
|
|
pb-init:
|
|
image: alpine:3.19
|
|
depends_on:
|
|
pocketbase:
|
|
condition: service_healthy
|
|
environment:
|
|
POCKETBASE_URL: "http://pocketbase:8090"
|
|
POCKETBASE_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL:-admin@libnovel.local}"
|
|
POCKETBASE_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD:-changeme123}"
|
|
volumes:
|
|
- ./scripts/pb-init.sh:/pb-init.sh:ro
|
|
entrypoint: ["sh", "/pb-init.sh"]
|
|
|
|
# ─── Browserless ────────────────────────────────────────────────────────────
|
|
browserless:
|
|
image: ghcr.io/browserless/chromium:latest
|
|
#container_name: libnovel-browserless
|
|
restart: unless-stopped
|
|
environment:
|
|
# Set a token to lock down the endpoint; the scraper reads it via
|
|
# BROWSERLESS_TOKEN below.
|
|
TOKEN: "${BROWSERLESS_TOKEN:-}"
|
|
# Allow up to 10 concurrent browser sessions.
|
|
CONCURRENT: "${BROWSERLESS_CONCURRENT:-10}"
|
|
# Queue up to 100 requests before returning 429.
|
|
QUEUED: "${BROWSERLESS_QUEUED:-100}"
|
|
# Per-session timeout in ms.
|
|
TIMEOUT: "${BROWSERLESS_TIMEOUT:-60000}"
|
|
# Optional webhook URL for Browserless error alerts.
|
|
ERROR_ALERT_URL: "${ERROR_ALERT_URL:-}"
|
|
ports:
|
|
- "${BROWSERLESS_PORT:-3030}:3000"
|
|
# Shared memory is required for Chrome.
|
|
shm_size: "2gb"
|
|
healthcheck:
|
|
test: ["CMD", "wget", "-qO-", "http://localhost:3000/json/version"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
# ─── Scraper ─────────────────────────────────────────────────────────────────
|
|
scraper:
|
|
build:
|
|
context: ./scraper
|
|
dockerfile: Dockerfile
|
|
#container_name: libnovel-scraper
|
|
restart: unless-stopped
|
|
depends_on:
|
|
pb-init:
|
|
condition: service_completed_successfully
|
|
pocketbase:
|
|
condition: service_healthy
|
|
minio:
|
|
condition: service_healthy
|
|
environment:
|
|
BROWSERLESS_URL: "http://browserless:3000"
|
|
BROWSERLESS_TOKEN: "${BROWSERLESS_TOKEN:-}"
|
|
# content | scrape | cdp | direct — used for catalogue, metadata, ranking.
|
|
# Chapter list and chapter text always use direct HTTP regardless of this setting.
|
|
BROWSERLESS_STRATEGY: "${BROWSERLESS_STRATEGY:-direct}"
|
|
# 0 → defaults to NumCPU inside the container.
|
|
SCRAPER_WORKERS: "${SCRAPER_WORKERS:-0}"
|
|
SCRAPER_HTTP_ADDR: ":8080"
|
|
LOG_LEVEL: "debug"
|
|
# Kokoro-FastAPI TTS endpoint.
|
|
KOKORO_URL: "${KOKORO_URL:-https://kokoro.kalekber.cc}"
|
|
KOKORO_VOICE: "${KOKORO_VOICE:-af_bella}"
|
|
# MinIO / S3 object storage
|
|
MINIO_ENDPOINT: "minio:9000"
|
|
MINIO_ACCESS_KEY: "${MINIO_ROOT_USER:-admin}"
|
|
MINIO_SECRET_KEY: "${MINIO_ROOT_PASSWORD:-changeme123}"
|
|
MINIO_USE_SSL: "false"
|
|
MINIO_BUCKET_CHAPTERS: "${MINIO_BUCKET_CHAPTERS:-libnovel-chapters}"
|
|
MINIO_BUCKET_AUDIO: "${MINIO_BUCKET_AUDIO:-libnovel-audio}"
|
|
# Public endpoint used to sign presigned audio URLs so browsers can reach them.
|
|
# Leave empty to use MINIO_ENDPOINT (fine for local dev).
|
|
MINIO_PUBLIC_ENDPOINT: "${MINIO_PUBLIC_ENDPOINT:-}"
|
|
MINIO_PUBLIC_USE_SSL: "${MINIO_PUBLIC_USE_SSL:-true}"
|
|
# PocketBase
|
|
POCKETBASE_URL: "http://pocketbase:8090"
|
|
POCKETBASE_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL:-admin@libnovel.local}"
|
|
POCKETBASE_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD:-changeme123}"
|
|
ports:
|
|
- "${SCRAPER_PORT:-8080}:8080"
|
|
healthcheck:
|
|
test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 3
|
|
|
|
# ─── SvelteKit UI ────────────────────────────────────────────────────────────
|
|
ui:
|
|
build:
|
|
context: ./ui
|
|
dockerfile: Dockerfile
|
|
# container_name: libnovel-ui
|
|
restart: unless-stopped
|
|
depends_on:
|
|
pb-init:
|
|
condition: service_completed_successfully
|
|
scraper:
|
|
condition: service_healthy
|
|
pocketbase:
|
|
condition: service_healthy
|
|
environment:
|
|
SCRAPER_API_URL: "http://scraper:8080"
|
|
POCKETBASE_URL: "http://pocketbase:8090"
|
|
POCKETBASE_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL:-admin@libnovel.local}"
|
|
POCKETBASE_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD:-changeme123}"
|
|
PUBLIC_MINIO_PUBLIC_URL: "${PUBLIC_MINIO_PUBLIC_URL:-http://localhost:9000}"
|
|
ports:
|
|
- "${UI_PORT:-5252}:3000"
|
|
healthcheck:
|
|
test: ["CMD", "wget", "-qO-", "http://localhost:3000/"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 3
|
|
|
|
volumes:
|
|
minio_data:
|
|
pb_data:
|