# LibNovel homelab runner # # Connects to production PocketBase and MinIO via public subdomains. # All secrets come from Doppler (project=libnovel, config=prd_homelab). # Run with: doppler run -- docker compose up -d # # Differs from prod runner: # - RUNNER_WORKER_ID=homelab-runner-1 (unique, avoids task claiming conflicts) # - MINIO_ENDPOINT/USE_SSL → storage.libnovel.cc over HTTPS # - POCKETBASE_URL → https://pb.libnovel.cc # - MEILI_URL → https://search.libnovel.cc (Caddy-proxied) # - VALKEY_ADDR → unset (not exposed publicly) # - RUNNER_SKIP_INITIAL_CATALOGUE_REFRESH=true # - REDIS_ADDR → rediss://redis.libnovel.cc:6380 (prod Redis via Caddy TLS proxy) # - LibreTranslate service for machine translation (internal network only) # # extra_hosts pins storage.libnovel.cc and pb.libnovel.cc to the prod server IP # (165.22.70.138) so that large PutObject uploads and PocketBase writes bypass # Cloudflare's 100-second proxy timeout entirely. TLS still terminates at Caddy # on prod; the TLS certificate is valid for the domain names so SNI works fine. services: libretranslate: image: libretranslate/libretranslate:latest restart: unless-stopped environment: LT_API_KEYS: "true" LT_API_KEYS_DB_PATH: "/app/db/api_keys.db" # Limit to source→target pairs the runner actually uses LT_LOAD_ONLY: "en,ru,id,pt,fr" LT_DISABLE_WEB_UI: "true" LT_UPDATE_MODELS: "false" volumes: - libretranslate_models:/home/libretranslate/.local/share/argos-translate - libretranslate_db:/app/db runner: image: kalekber/libnovel-runner:latest restart: unless-stopped stop_grace_period: 135s labels: - "com.centurylinklabs.watchtower.enable=true" depends_on: - libretranslate # Pin prod subdomains to the prod server IP to bypass Cloudflare's 100s # proxy timeout. Large MP3 PutObject uploads and PocketBase writes go # directly to Caddy on prod; TLS and SNI still work normally. extra_hosts: - "storage.libnovel.cc:165.22.70.138" - "pb.libnovel.cc:165.22.70.138" environment: # ── PocketBase ────────────────────────────────────────────────────────── POCKETBASE_URL: "https://pb.libnovel.cc" POCKETBASE_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL}" POCKETBASE_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD}" # ── MinIO (S3 API via public subdomain) ───────────────────────────────── MINIO_ENDPOINT: "storage.libnovel.cc" MINIO_ACCESS_KEY: "${MINIO_ROOT_USER}" MINIO_SECRET_KEY: "${MINIO_ROOT_PASSWORD}" MINIO_USE_SSL: "true" MINIO_PUBLIC_ENDPOINT: "${MINIO_PUBLIC_ENDPOINT}" MINIO_PUBLIC_USE_SSL: "${MINIO_PUBLIC_USE_SSL}" # ── Meilisearch (via search.libnovel.cc Caddy proxy) ──────────────────── MEILI_URL: "${MEILI_URL}" MEILI_API_KEY: "${MEILI_API_KEY}" VALKEY_ADDR: "" # Force IPv4 DNS resolution — homelab has no IPv6 route to search.libnovel.cc GODEBUG: "preferIPv4=1" # ── Kokoro TTS ────────────────────────────────────────────────────────── KOKORO_URL: "${KOKORO_URL}" KOKORO_VOICE: "${KOKORO_VOICE}" # ── Pocket TTS ────────────────────────────────────────────────────────── POCKET_TTS_URL: "${POCKET_TTS_URL}" # ── Cloudflare Workers AI TTS ──────────────────────────────────────────── CFAI_ACCOUNT_ID: "${CFAI_ACCOUNT_ID}" CFAI_API_TOKEN: "${CFAI_API_TOKEN}" # ── LibreTranslate (internal Docker network) ──────────────────────────── LIBRETRANSLATE_URL: "http://libretranslate:5000" LIBRETRANSLATE_API_KEY: "${LIBRETRANSLATE_API_KEY}" # ── Asynq / Redis (prod Redis via Caddy TLS proxy) ────────────────────── # The runner connects to prod Redis over TLS: rediss://redis.libnovel.cc:6380. # Caddy on prod terminates TLS and proxies to the local redis:6379 sidecar. REDIS_ADDR: "${REDIS_ADDR}" REDIS_PASSWORD: "${REDIS_PASSWORD}" # ── Runner tuning ─────────────────────────────────────────────────────── RUNNER_WORKER_ID: "${RUNNER_WORKER_ID}" RUNNER_POLL_INTERVAL: "${RUNNER_POLL_INTERVAL}" RUNNER_MAX_CONCURRENT_SCRAPE: "${RUNNER_MAX_CONCURRENT_SCRAPE}" RUNNER_MAX_CONCURRENT_AUDIO: "${RUNNER_MAX_CONCURRENT_AUDIO}" RUNNER_MAX_CONCURRENT_TRANSLATION: "${RUNNER_MAX_CONCURRENT_TRANSLATION}" RUNNER_TIMEOUT: "${RUNNER_TIMEOUT}" RUNNER_METRICS_ADDR: "${RUNNER_METRICS_ADDR}" RUNNER_SKIP_INITIAL_CATALOGUE_REFRESH: "true" # ── Observability ─────────────────────────────────────────────────────── LOG_LEVEL: "${LOG_LEVEL}" GLITCHTIP_DSN: "${GLITCHTIP_DSN_RUNNER}" healthcheck: test: ["CMD", "/healthcheck", "file", "/tmp/runner.alive", "120"] interval: 60s timeout: 5s retries: 3 volumes: libretranslate_models: libretranslate_db: