# 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 service for Asynq task queue (local to homelab, exposed to prod via Caddy TCP proxy) services: redis: image: redis:7-alpine restart: unless-stopped volumes: - redis_data:/data command: > redis-server --appendonly yes --requirepass "${REDIS_PASSWORD}" healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 10s timeout: 5s retries: 5 runner: image: kalekber/libnovel-runner:latest restart: unless-stopped stop_grace_period: 135s depends_on: redis: condition: service_healthy 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}" # ── Asynq / Redis (local service) ─────────────────────────────────────── # The runner connects to the local Redis sidecar. REDIS_ADDR: "redis:6379" 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_TIMEOUT: "${RUNNER_TIMEOUT}" RUNNER_METRICS_ADDR: "${RUNNER_METRICS_ADDR}" RUNNER_SKIP_INITIAL_CATALOGUE_REFRESH: "true" # ── Observability ─────────────────────────────────────────────────────── LOG_LEVEL: "${LOG_LEVEL}" GLITCHTIP_DSN: "${GLITCHTIP_DSN}" healthcheck: test: ["CMD", "/healthcheck", "file", "/tmp/runner.alive", "120"] interval: 60s timeout: 5s retries: 3 volumes: redis_data: