All checks were successful
Release / Scraper / Test (push) Successful in 20s
Release / UI / Build (push) Successful in 25s
CI / Scraper / Lint (pull_request) Successful in 10s
Release / v2 / Build ui-v2 (push) Successful in 28s
CI / Scraper / Test (pull_request) Successful in 15s
CI / UI / Build (pull_request) Successful in 25s
Release / Scraper / Docker (push) Successful in 55s
Release / UI / Docker (push) Successful in 43s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (pull_request) Has been skipped
Release / v2 / Docker / ui-v2 (push) Successful in 36s
Release / v2 / Test backend (push) Successful in 3m51s
Release / v2 / Docker / runner (push) Successful in 36s
Release / v2 / Docker / backend (push) Successful in 1m34s
iOS CI / Build (pull_request) Successful in 5m33s
iOS CI / Test (pull_request) Successful in 11m25s
- Runner fetches 9 browse combos (genre×sort×status) every 6h and stores JSON snapshots in MinIO libnovel-browse bucket (browse_refresh.go) - Backend handleBrowse reads page-1 results from MinIO first; falls back to live novelfire.net fetch; returns empty+cached:false on total failure instead of 502 - Add BrowseStore interface (bookstore.go), MinIO put/get helpers (minio.go), Store methods + compile-time assertion (store.go), BucketBrowse config, wiring in cmd/backend and cmd/runner, docker-compose-new bucket init - Fix ReapStaleTasks: PocketBase datetime fields require heartbeat_at=null (not heartbeat_at="") in filter expressions, and nil (not "") in patch payload — was causing 400 errors on every reap cycle
212 lines
8.4 KiB
YAML
212 lines
8.4 KiB
YAML
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;
|
||
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) ─────────────────────────────────────
|
||
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_BUCKET_BROWSE: "${MINIO_BUCKET_BROWSE:-libnovel-browse}"
|
||
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_BUCKET_BROWSE: "${MINIO_BUCKET_BROWSE:-libnovel-browse}"
|
||
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:
|