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; mc mb --ignore-existing local/libnovel-browse; 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}" MINIO_BUCKET_BROWSE: "${MINIO_BUCKET_BROWSE:-libnovel-browse}" # 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}" # SingleFile CLI path for save-browse subcommand SINGLEFILE_PATH: "${SINGLEFILE_PATH:-single-file}" # 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}" MINIO_PUBLIC_ENDPOINT: "${MINIO_PUBLIC_ENDPOINT:-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: