services: # ─── MinIO (object storage: chapters, audio, avatars) ──────────────────────── minio: image: minio/minio:latest 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 ───────────────────────────────────────────── minio-init: image: minio/mc:latest 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; mc mb --ignore-existing local/libnovel-avatars; echo 'buckets ready'; " environment: MINIO_ROOT_USER: "${MINIO_ROOT_USER:-admin}" MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:-changeme123}" # ─── PocketBase (auth + structured data) ───────────────────────────────────── pocketbase: image: ghcr.io/muchobien/pocketbase:latest restart: unless-stopped environment: 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 ───────────────────────────────────────── 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"] # ─── Backend API ────────────────────────────────────────────────────────────── backend: build: context: ./backend dockerfile: Dockerfile target: backend args: VERSION: "${GIT_TAG:-dev}" COMMIT: "${GIT_COMMIT:-unknown}" restart: unless-stopped stop_grace_period: 35s depends_on: pb-init: condition: service_completed_successfully pocketbase: condition: service_healthy minio: condition: service_healthy environment: BACKEND_HTTP_ADDR: ":8080" LOG_LEVEL: "${LOG_LEVEL:-info}" # MinIO 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}" MINIO_BUCKET_AVATARS: "${MINIO_BUCKET_AVATARS:-libnovel-avatars}" MINIO_PUBLIC_ENDPOINT: "${MINIO_PUBLIC_ENDPOINT:-http://localhost:9000}" MINIO_PUBLIC_USE_SSL: "${MINIO_PUBLIC_USE_SSL:-false}" # PocketBase POCKETBASE_URL: "http://pocketbase:8090" POCKETBASE_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL:-admin@libnovel.local}" POCKETBASE_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD:-changeme123}" ports: - "${BACKEND_PORT:-8080}:8080" healthcheck: test: ["CMD", "/healthcheck", "http://localhost:8080/health"] interval: 15s timeout: 5s retries: 3 # ─── Runner (background task worker) ───────────────────────────────────────── runner: build: context: ./backend dockerfile: Dockerfile target: runner args: VERSION: "${GIT_TAG:-dev}" COMMIT: "${GIT_COMMIT:-unknown}" restart: unless-stopped stop_grace_period: 135s depends_on: pb-init: condition: service_completed_successfully pocketbase: condition: service_healthy minio: condition: service_healthy environment: LOG_LEVEL: "${LOG_LEVEL:-info}" # Runner tuning RUNNER_POLL_INTERVAL: "${RUNNER_POLL_INTERVAL:-30s}" # RUNNER_MAX_CONCURRENT_SCRAPE controls how many books are scraped in parallel. # Default is 1 (sequential). Increase for faster catalogue scrapes at the # cost of higher CPU/network load on the novelfire.net target. RUNNER_MAX_CONCURRENT_SCRAPE: "${RUNNER_MAX_CONCURRENT_SCRAPE:-1}" RUNNER_MAX_CONCURRENT_AUDIO: "${RUNNER_MAX_CONCURRENT_AUDIO:-1}" RUNNER_WORKER_ID: "${RUNNER_WORKER_ID:-runner-1}" RUNNER_WORKERS: "${RUNNER_WORKERS:-0}" RUNNER_TIMEOUT: "${RUNNER_TIMEOUT:-90s}" SCRAPER_PROXY: "${SCRAPER_PROXY:-}" # Kokoro-FastAPI TTS endpoint KOKORO_URL: "${KOKORO_URL:-https://kokoro.kalekber.cc}" KOKORO_VOICE: "${KOKORO_VOICE:-af_bella}" # MinIO 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}" MINIO_BUCKET_AVATARS: "${MINIO_BUCKET_AVATARS:-libnovel-avatars}" MINIO_PUBLIC_ENDPOINT: "${MINIO_PUBLIC_ENDPOINT:-http://localhost:9000}" MINIO_PUBLIC_USE_SSL: "${MINIO_PUBLIC_USE_SSL:-false}" # PocketBase POCKETBASE_URL: "http://pocketbase:8090" POCKETBASE_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL:-admin@libnovel.local}" POCKETBASE_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD:-changeme123}" healthcheck: # The runner has no HTTP server. It writes /tmp/runner.alive on every poll. # 120s = 2× the default 30s poll interval with generous headroom. test: ["CMD", "/healthcheck", "file", "/tmp/runner.alive", "120"] interval: 60s timeout: 5s retries: 3 # ─── SvelteKit UI ───────────────────────────────────────────────────────────── ui: build: context: ./ui-v2 dockerfile: Dockerfile args: BUILD_VERSION: "${GIT_TAG:-dev}" BUILD_COMMIT: "${GIT_COMMIT:-unknown}" restart: unless-stopped stop_grace_period: 35s depends_on: pb-init: condition: service_completed_successfully backend: condition: service_healthy pocketbase: condition: service_healthy environment: # ORIGIN must match the URL the browser uses to reach the UI. # adapter-node uses this for SvelteKit's built-in CSRF origin check. # When running behind a reverse proxy or non-standard port, set this via # the ORIGIN env var (e.g. https://libnovel.example.com). ORIGIN: "${ORIGIN:-http://localhost:5252}" SCRAPER_API_URL: "http://backend:8080" POCKETBASE_URL: "http://pocketbase:8090" POCKETBASE_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL:-admin@libnovel.local}" POCKETBASE_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD:-changeme123}" AUTH_SECRET: "${AUTH_SECRET:-dev_secret_change_in_production}" PUBLIC_MINIO_PUBLIC_URL: "${MINIO_PUBLIC_ENDPOINT:-http://localhost:9000}" ports: - "${UI_PORT:-5252}:3000" healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"] interval: 15s timeout: 5s retries: 3 volumes: minio_data: pb_data: