Some checks failed
CI / Test backend (pull_request) Failing after 11s
CI / Check ui (pull_request) Failing after 11s
CI / Docker / backend (pull_request) Has been skipped
CI / Docker / runner (pull_request) Has been skipped
CI / Docker / ui (pull_request) Has been skipped
CI / Docker / caddy (pull_request) Successful in 2m53s
Release / Test backend (push) Successful in 19s
Release / Check ui (push) Successful in 33s
Release / Docker / caddy (push) Failing after 1m26s
Release / Docker / ui (push) Failing after 11s
Release / Docker / backend (push) Successful in 2m2s
Release / Docker / runner (push) Successful in 2m24s
Release / Gitea Release (push) Has been skipped
- Add email/email_verified/verification_token/verification_token_exp fields
to app_users PocketBase schema (pb-init-v3.sh)
- Add SMTP env vars to UI service in docker-compose.yml
- New email.ts: raw TLS SMTP mailer via Node tls module, sendVerificationEmail()
- createUser() now takes email param, stores verification token (24h TTL)
- loginUser() throws 'Email not verified' when email_verified is false
- New /verify-email route: validates token, verifies user, auto-logs in
- Login page: email field in register form, check-inbox state after register
- /api/auth/register (iOS): returns { pending_verification, email } instead of token
- Add pb.libnovel.cc and storage.libnovel.cc Caddy virtual hosts for homelab runner
- Add homelab runner docker-compose and libnovel.sh helper script
594 lines
22 KiB
YAML
594 lines
22 KiB
YAML
# ── Shared environment fragments ──────────────────────────────────────────────
|
||
# These YAML anchors eliminate duplication between backend and runner.
|
||
# All values come from Doppler — no fallbacks needed here.
|
||
# Run commands via: just up / just build / etc. (see justfile)
|
||
x-infra-env: &infra-env
|
||
# MinIO
|
||
MINIO_ENDPOINT: "minio:9000"
|
||
MINIO_ACCESS_KEY: "${MINIO_ROOT_USER}"
|
||
MINIO_SECRET_KEY: "${MINIO_ROOT_PASSWORD}"
|
||
MINIO_USE_SSL: "false"
|
||
MINIO_PUBLIC_ENDPOINT: "${MINIO_PUBLIC_ENDPOINT}"
|
||
MINIO_PUBLIC_USE_SSL: "${MINIO_PUBLIC_USE_SSL}"
|
||
# PocketBase
|
||
POCKETBASE_URL: "http://pocketbase:8090"
|
||
POCKETBASE_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL}"
|
||
POCKETBASE_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD}"
|
||
# Meilisearch
|
||
MEILI_URL: "http://meilisearch:7700"
|
||
MEILI_API_KEY: "${MEILI_MASTER_KEY}"
|
||
# Valkey
|
||
VALKEY_ADDR: "valkey:6379"
|
||
|
||
services:
|
||
# ─── MinIO (object storage: chapters, audio, avatars, browse) ────────────────
|
||
minio:
|
||
image: minio/minio:latest
|
||
restart: unless-stopped
|
||
command: server /data --console-address ":9001"
|
||
environment:
|
||
MINIO_ROOT_USER: "${MINIO_ROOT_USER}"
|
||
MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD}"
|
||
# No public port — all presigned URL traffic goes through backend or a
|
||
# separately-exposed MINIO_PUBLIC_ENDPOINT (e.g. storage.libnovel.cc).
|
||
expose:
|
||
- "9000"
|
||
- "9001"
|
||
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} $${MINIO_ROOT_PASSWORD};
|
||
mc mb --ignore-existing local/chapters;
|
||
mc mb --ignore-existing local/audio;
|
||
mc mb --ignore-existing local/avatars;
|
||
mc mb --ignore-existing local/catalogue;
|
||
echo 'buckets ready';
|
||
"
|
||
environment:
|
||
MINIO_ROOT_USER: "${MINIO_ROOT_USER}"
|
||
MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD}"
|
||
|
||
# ─── PocketBase (auth + structured data) ─────────────────────────────────────
|
||
pocketbase:
|
||
image: ghcr.io/muchobien/pocketbase:latest
|
||
restart: unless-stopped
|
||
environment:
|
||
PB_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL}"
|
||
PB_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD}"
|
||
# No public port — accessed only by backend/runner on the internal network.
|
||
expose:
|
||
- "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}"
|
||
POCKETBASE_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD}"
|
||
volumes:
|
||
- ./scripts/pb-init-v3.sh:/pb-init.sh:ro
|
||
entrypoint: ["sh", "/pb-init.sh"]
|
||
|
||
# ─── Meilisearch (full-text search) ──────────────────────────────────────────
|
||
meilisearch:
|
||
image: getmeili/meilisearch:latest
|
||
restart: unless-stopped
|
||
environment:
|
||
MEILI_MASTER_KEY: "${MEILI_MASTER_KEY}"
|
||
MEILI_ENV: "${MEILI_ENV}"
|
||
# No public port — backend/runner reach it via internal network.
|
||
expose:
|
||
- "7700"
|
||
volumes:
|
||
- meili_data:/meili_data
|
||
healthcheck:
|
||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:7700/health"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
# ─── Valkey (presign URL cache) ───────────────────────────────────────────────
|
||
valkey:
|
||
image: valkey/valkey:7-alpine
|
||
restart: unless-stopped
|
||
# No public port — backend/runner/ui reach it via internal network.
|
||
expose:
|
||
- "6379"
|
||
volumes:
|
||
- valkey_data:/data
|
||
healthcheck:
|
||
test: ["CMD", "valkey-cli", "ping"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
# ─── Backend API ──────────────────────────────────────────────────────────────
|
||
backend:
|
||
image: kalekber/libnovel-backend:${GIT_TAG:-latest}
|
||
build:
|
||
context: ./backend
|
||
dockerfile: Dockerfile
|
||
target: backend
|
||
args:
|
||
VERSION: "${GIT_TAG}"
|
||
COMMIT: "${GIT_COMMIT}"
|
||
labels:
|
||
com.centurylinklabs.watchtower.enable: "true"
|
||
restart: unless-stopped
|
||
stop_grace_period: 35s
|
||
depends_on:
|
||
pb-init:
|
||
condition: service_completed_successfully
|
||
pocketbase:
|
||
condition: service_healthy
|
||
minio:
|
||
condition: service_healthy
|
||
meilisearch:
|
||
condition: service_healthy
|
||
valkey:
|
||
condition: service_healthy
|
||
# No public port — all traffic is routed via Caddy.
|
||
expose:
|
||
- "8080"
|
||
environment:
|
||
<<: *infra-env
|
||
BACKEND_HTTP_ADDR: ":8080"
|
||
LOG_LEVEL: "${LOG_LEVEL}"
|
||
KOKORO_URL: "${KOKORO_URL}"
|
||
KOKORO_VOICE: "${KOKORO_VOICE}"
|
||
GLITCHTIP_DSN: "${GLITCHTIP_DSN}"
|
||
healthcheck:
|
||
test: ["CMD", "/healthcheck", "http://localhost:8080/health"]
|
||
interval: 15s
|
||
timeout: 5s
|
||
retries: 3
|
||
|
||
# ─── Runner (background task worker) ─────────────────────────────────────────
|
||
# profiles: [runner] prevents accidental restart on `docker compose up -d`.
|
||
# The homelab runner (192.168.0.109) is the sole worker in production.
|
||
# To start explicitly: doppler run -- docker compose --profile runner up -d runner
|
||
runner:
|
||
profiles: [runner]
|
||
image: kalekber/libnovel-runner:${GIT_TAG:-latest}
|
||
build:
|
||
context: ./backend
|
||
dockerfile: Dockerfile
|
||
target: runner
|
||
args:
|
||
VERSION: "${GIT_TAG}"
|
||
COMMIT: "${GIT_COMMIT}"
|
||
labels:
|
||
com.centurylinklabs.watchtower.enable: "true"
|
||
restart: unless-stopped
|
||
stop_grace_period: 135s
|
||
depends_on:
|
||
pb-init:
|
||
condition: service_completed_successfully
|
||
pocketbase:
|
||
condition: service_healthy
|
||
minio:
|
||
condition: service_healthy
|
||
meilisearch:
|
||
condition: service_healthy
|
||
valkey:
|
||
condition: service_healthy
|
||
# Metrics endpoint — internal only; expose publicly via Caddy if needed.
|
||
expose:
|
||
- "9091"
|
||
environment:
|
||
<<: *infra-env
|
||
LOG_LEVEL: "${LOG_LEVEL}"
|
||
# Runner tuning
|
||
RUNNER_POLL_INTERVAL: "${RUNNER_POLL_INTERVAL}"
|
||
RUNNER_MAX_CONCURRENT_SCRAPE: "${RUNNER_MAX_CONCURRENT_SCRAPE}"
|
||
RUNNER_MAX_CONCURRENT_AUDIO: "${RUNNER_MAX_CONCURRENT_AUDIO}"
|
||
RUNNER_WORKER_ID: "${RUNNER_WORKER_ID}"
|
||
RUNNER_TIMEOUT: "${RUNNER_TIMEOUT}"
|
||
RUNNER_METRICS_ADDR: "${RUNNER_METRICS_ADDR}"
|
||
# Suppress the on-startup catalogue walk — catalogue_refresh now skips
|
||
# books already in Meilisearch, so a full walk on every restart is wasteful.
|
||
# The 24h periodic ticker (CatalogueRefreshInterval) still fires normally.
|
||
RUNNER_SKIP_INITIAL_CATALOGUE_REFRESH: "true"
|
||
# Kokoro-FastAPI TTS endpoint
|
||
KOKORO_URL: "${KOKORO_URL}"
|
||
KOKORO_VOICE: "${KOKORO_VOICE}"
|
||
GLITCHTIP_DSN: "${GLITCHTIP_DSN}"
|
||
healthcheck:
|
||
# The runner 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:
|
||
image: kalekber/libnovel-ui:${GIT_TAG:-latest}
|
||
build:
|
||
context: ./ui
|
||
dockerfile: Dockerfile
|
||
args:
|
||
BUILD_VERSION: "${GIT_TAG}"
|
||
BUILD_COMMIT: "${GIT_COMMIT}"
|
||
labels:
|
||
com.centurylinklabs.watchtower.enable: "true"
|
||
restart: unless-stopped
|
||
stop_grace_period: 35s
|
||
depends_on:
|
||
pb-init:
|
||
condition: service_completed_successfully
|
||
backend:
|
||
condition: service_healthy
|
||
pocketbase:
|
||
condition: service_healthy
|
||
valkey:
|
||
condition: service_healthy
|
||
# No public port — all traffic via Caddy.
|
||
expose:
|
||
- "3000"
|
||
environment:
|
||
# ORIGIN must match the public URL Caddy serves on.
|
||
# adapter-node uses this for SvelteKit's built-in CSRF origin check.
|
||
ORIGIN: "${ORIGIN}"
|
||
BACKEND_API_URL: "http://backend:8080"
|
||
POCKETBASE_URL: "http://pocketbase:8090"
|
||
POCKETBASE_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL}"
|
||
POCKETBASE_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD}"
|
||
AUTH_SECRET: "${AUTH_SECRET}"
|
||
PUBLIC_MINIO_PUBLIC_URL: "${MINIO_PUBLIC_ENDPOINT}"
|
||
# Valkey
|
||
VALKEY_ADDR: "valkey:6379"
|
||
# Umami analytics
|
||
PUBLIC_UMAMI_WEBSITE_ID: "${PUBLIC_UMAMI_WEBSITE_ID}"
|
||
PUBLIC_UMAMI_SCRIPT_URL: "${PUBLIC_UMAMI_SCRIPT_URL}"
|
||
# GlitchTip client + server-side error tracking
|
||
PUBLIC_GLITCHTIP_DSN: "${PUBLIC_GLITCHTIP_DSN}"
|
||
# Email verification (Resend SMTP — shared with Fider/GlitchTip)
|
||
SMTP_HOST: "${FIDER_SMTP_HOST}"
|
||
SMTP_PORT: "${FIDER_SMTP_PORT}"
|
||
SMTP_USER: "${FIDER_SMTP_USER}"
|
||
SMTP_PASSWORD: "${FIDER_SMTP_PASSWORD}"
|
||
SMTP_FROM: "noreply@libnovel.cc"
|
||
APP_URL: "${ORIGIN}"
|
||
healthcheck:
|
||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
|
||
interval: 15s
|
||
timeout: 5s
|
||
retries: 3
|
||
|
||
# ─── CrowdSec (threat detection + IP blocking) ───────────────────────────────
|
||
# Reads Caddy JSON access logs from the shared caddy_logs volume and enforces
|
||
# decisions via the Caddy bouncer plugin.
|
||
crowdsec:
|
||
image: crowdsecurity/crowdsec:latest
|
||
restart: unless-stopped
|
||
environment:
|
||
GID: "1000"
|
||
COLLECTIONS: "crowdsecurity/caddy crowdsecurity/http-dos crowdsecurity/base-http-scenarios"
|
||
volumes:
|
||
- crowdsec_data:/var/lib/crowdsec/data
|
||
- ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
||
- caddy_logs:/var/log/caddy:ro
|
||
expose:
|
||
- "8080"
|
||
healthcheck:
|
||
test: ["CMD", "cscli", "version"]
|
||
interval: 20s
|
||
timeout: 10s
|
||
retries: 5
|
||
|
||
# ─── CrowdSec bouncer registration ───────────────────────────────────────────
|
||
# One-shot: registers the Caddy bouncer with the CrowdSec LAPI and writes the
|
||
# generated API key to crowdsec/.crowdsec.env, which Caddy reads via env_file.
|
||
# Uses the Docker socket to exec cscli inside the running crowdsec container.
|
||
crowdsec-init:
|
||
image: docker:cli
|
||
depends_on:
|
||
crowdsec:
|
||
condition: service_healthy
|
||
volumes:
|
||
- /var/run/docker.sock:/var/run/docker.sock
|
||
- ./crowdsec:/crowdsec-out
|
||
entrypoint: >
|
||
/bin/sh -c "
|
||
out=/crowdsec-out/.crowdsec.env;
|
||
existing=$$(grep -s '^CROWDSEC_API_KEY=.' \"$$out\" | cut -d= -f2-);
|
||
if [ -n \"$$existing\" ]; then
|
||
echo 'crowdsec-init: key already present, skipping registration';
|
||
exit 0;
|
||
fi;
|
||
container=$$(docker ps --filter name=crowdsec --filter status=running --format '{{.Names}}' | grep -v init | head -1);
|
||
echo \"crowdsec-init: using container $$container\";
|
||
docker exec $$container cscli bouncers delete caddy-bouncer 2>/dev/null || true;
|
||
key=$$(docker exec $$container cscli bouncers add caddy-bouncer -o raw 2>&1);
|
||
if [ -z \"$$key\" ]; then
|
||
echo 'crowdsec-init: ERROR — failed to obtain bouncer key' >&2;
|
||
exit 1;
|
||
fi;
|
||
printf 'CROWDSEC_API_KEY=%s\n' \"$$key\" > \"$$out\";
|
||
echo \"crowdsec-init: bouncer key written (key length: $${#key})\";
|
||
"
|
||
restart: "no"
|
||
|
||
|
||
# ─── Caddy (reverse proxy + automatic HTTPS) ──────────────────────────────────
|
||
# Custom build includes github.com/mholt/caddy-ratelimit and
|
||
# github.com/hslatman/caddy-crowdsec-bouncer/http.
|
||
caddy:
|
||
image: kalekber/libnovel-caddy:${GIT_TAG:-latest}
|
||
build:
|
||
context: ./caddy
|
||
dockerfile: Dockerfile
|
||
restart: unless-stopped
|
||
depends_on:
|
||
backend:
|
||
condition: service_healthy
|
||
ui:
|
||
condition: service_healthy
|
||
crowdsec-init:
|
||
condition: service_completed_successfully
|
||
ports:
|
||
- "80:80"
|
||
- "443:443"
|
||
- "443:443/udp" # HTTP/3 (QUIC)
|
||
environment:
|
||
DOMAIN: "${DOMAIN}"
|
||
CADDY_ACME_EMAIL: "${CADDY_ACME_EMAIL}"
|
||
env_file:
|
||
- path: ./crowdsec/.crowdsec.env
|
||
required: false
|
||
volumes:
|
||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||
- ./caddy/errors:/srv/errors:ro
|
||
- caddy_data:/data
|
||
- caddy_config:/config
|
||
- caddy_logs:/var/log/caddy
|
||
|
||
# ─── Watchtower (auto-redeploy custom services on new images) ────────────────
|
||
# Only watches services labelled com.centurylinklabs.watchtower.enable=true.
|
||
# Third-party infra images (minio, pocketbase, meilisearch, etc.) are excluded.
|
||
watchtower:
|
||
image: containrrr/watchtower:latest
|
||
restart: unless-stopped
|
||
volumes:
|
||
- /var/run/docker.sock:/var/run/docker.sock
|
||
command: --label-enable --interval 300 --cleanup
|
||
environment:
|
||
WATCHTOWER_NOTIFICATIONS: "${WATCHTOWER_NOTIFICATIONS}"
|
||
WATCHTOWER_NOTIFICATION_URL: "${WATCHTOWER_NOTIFICATION_URL}"
|
||
DOCKER_API_VERSION: "1.44"
|
||
|
||
# ─── Shared PostgreSQL (Fider + GlitchTip + Umami) ───────────────────────────
|
||
# A single Postgres instance hosting three separate databases.
|
||
# PocketBase uses its own embedded SQLite; this postgres is only for the
|
||
# three new services below.
|
||
postgres:
|
||
image: postgres:16-alpine
|
||
restart: unless-stopped
|
||
environment:
|
||
POSTGRES_USER: "${POSTGRES_USER}"
|
||
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
|
||
POSTGRES_DB: postgres
|
||
expose:
|
||
- "5432"
|
||
volumes:
|
||
- postgres_data:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER}"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
# ─── Postgres database initialisation ────────────────────────────────────────
|
||
# One-shot: creates the fider, glitchtip, and umami databases if missing.
|
||
postgres-init:
|
||
image: postgres:16-alpine
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
environment:
|
||
PGPASSWORD: "${POSTGRES_PASSWORD}"
|
||
entrypoint: >
|
||
/bin/sh -c "
|
||
psql -h postgres -U ${POSTGRES_USER} -d postgres -tc \"SELECT 1 FROM pg_database WHERE datname='fider'\" | grep -q 1 ||
|
||
psql -h postgres -U ${POSTGRES_USER} -d postgres -c \"CREATE DATABASE fider\";
|
||
psql -h postgres -U ${POSTGRES_USER} -d postgres -tc \"SELECT 1 FROM pg_database WHERE datname='glitchtip'\" | grep -q 1 ||
|
||
psql -h postgres -U ${POSTGRES_USER} -d postgres -c \"CREATE DATABASE glitchtip\";
|
||
psql -h postgres -U ${POSTGRES_USER} -d postgres -tc \"SELECT 1 FROM pg_database WHERE datname='umami'\" | grep -q 1 ||
|
||
psql -h postgres -U ${POSTGRES_USER} -d postgres -c \"CREATE DATABASE umami\";
|
||
echo 'postgres-init: databases ready';
|
||
"
|
||
restart: "no"
|
||
|
||
# ─── Fider (user feedback & feature requests) ─────────────────────────────────
|
||
fider:
|
||
image: getfider/fider:stable
|
||
restart: unless-stopped
|
||
depends_on:
|
||
postgres-init:
|
||
condition: service_completed_successfully
|
||
postgres:
|
||
condition: service_healthy
|
||
expose:
|
||
- "3000"
|
||
environment:
|
||
BASE_URL: "${FIDER_BASE_URL}"
|
||
DATABASE_URL: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/fider?sslmode=disable"
|
||
JWT_SECRET: "${FIDER_JWT_SECRET}"
|
||
# Email: Resend SMTP
|
||
EMAIL_NOREPLY: "noreply@libnovel.cc"
|
||
EMAIL_SMTP_HOST: "${FIDER_SMTP_HOST}"
|
||
EMAIL_SMTP_PORT: "${FIDER_SMTP_PORT}"
|
||
EMAIL_SMTP_USERNAME: "${FIDER_SMTP_USER}"
|
||
EMAIL_SMTP_PASSWORD: "${FIDER_SMTP_PASSWORD}"
|
||
EMAIL_SMTP_ENABLE_STARTTLS: "false"
|
||
|
||
# ─── GlitchTip DB migration (one-shot) ───────────────────────────────────────
|
||
glitchtip-migrate:
|
||
image: glitchtip/glitchtip:latest
|
||
depends_on:
|
||
postgres-init:
|
||
condition: service_completed_successfully
|
||
postgres:
|
||
condition: service_healthy
|
||
environment:
|
||
DATABASE_URL: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/glitchtip"
|
||
SECRET_KEY: "${GLITCHTIP_SECRET_KEY}"
|
||
GLITCHTIP_DOMAIN: "${GLITCHTIP_DOMAIN}"
|
||
EMAIL_URL: "${GLITCHTIP_EMAIL_URL}"
|
||
DEFAULT_FROM_EMAIL: "noreply@libnovel.cc"
|
||
VALKEY_URL: "redis://valkey:6379/1"
|
||
command: "./manage.py migrate"
|
||
restart: "no"
|
||
|
||
# ─── GlitchTip web (error tracking UI + API) ─────────────────────────────────
|
||
glitchtip-web:
|
||
image: glitchtip/glitchtip:latest
|
||
restart: unless-stopped
|
||
depends_on:
|
||
glitchtip-migrate:
|
||
condition: service_completed_successfully
|
||
valkey:
|
||
condition: service_healthy
|
||
expose:
|
||
- "8000"
|
||
environment:
|
||
DATABASE_URL: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/glitchtip"
|
||
SECRET_KEY: "${GLITCHTIP_SECRET_KEY}"
|
||
GLITCHTIP_DOMAIN: "${GLITCHTIP_DOMAIN}"
|
||
EMAIL_URL: "${GLITCHTIP_EMAIL_URL}"
|
||
DEFAULT_FROM_EMAIL: "noreply@libnovel.cc"
|
||
VALKEY_URL: "redis://valkey:6379/1"
|
||
PORT: "8000"
|
||
ENABLE_USER_REGISTRATION: "false"
|
||
healthcheck:
|
||
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/0/')"]
|
||
interval: 15s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
# ─── GlitchTip worker (background task processor) ─────────────────────────────
|
||
glitchtip-worker:
|
||
image: glitchtip/glitchtip:latest
|
||
restart: unless-stopped
|
||
depends_on:
|
||
glitchtip-migrate:
|
||
condition: service_completed_successfully
|
||
valkey:
|
||
condition: service_healthy
|
||
environment:
|
||
DATABASE_URL: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/glitchtip"
|
||
SECRET_KEY: "${GLITCHTIP_SECRET_KEY}"
|
||
GLITCHTIP_DOMAIN: "${GLITCHTIP_DOMAIN}"
|
||
EMAIL_URL: "${GLITCHTIP_EMAIL_URL}"
|
||
DEFAULT_FROM_EMAIL: "noreply@libnovel.cc"
|
||
VALKEY_URL: "redis://valkey:6379/1"
|
||
SERVER_ROLE: "worker"
|
||
|
||
# ─── Umami (page analytics) ───────────────────────────────────────────────────
|
||
umami:
|
||
image: ghcr.io/umami-software/umami:postgresql-latest
|
||
restart: unless-stopped
|
||
depends_on:
|
||
postgres-init:
|
||
condition: service_completed_successfully
|
||
postgres:
|
||
condition: service_healthy
|
||
expose:
|
||
- "3000"
|
||
environment:
|
||
DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/umami"
|
||
APP_SECRET: "${UMAMI_APP_SECRET}"
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-sf", "http://localhost:3000/api/heartbeat"]
|
||
interval: 15s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
# ─── Dozzle (Docker log viewer) ───────────────────────────────────────────────
|
||
dozzle:
|
||
image: amir20/dozzle:latest
|
||
restart: unless-stopped
|
||
volumes:
|
||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||
- ./dozzle/users.yml:/data/users.yml:ro
|
||
expose:
|
||
- "8080"
|
||
environment:
|
||
DOZZLE_AUTH_PROVIDER: simple
|
||
DOZZLE_HOSTNAME: "logs.libnovel.cc"
|
||
healthcheck:
|
||
test: ["CMD", "/dozzle", "healthcheck"]
|
||
interval: 15s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
# ─── Uptime Kuma (uptime monitoring) ──────────────────────────────────────────
|
||
uptime-kuma:
|
||
image: louislam/uptime-kuma:1
|
||
restart: unless-stopped
|
||
volumes:
|
||
- uptime_kuma_data:/app/data
|
||
expose:
|
||
- "3001"
|
||
healthcheck:
|
||
test: ["CMD", "extra/healthcheck"]
|
||
interval: 15s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
# ─── Gotify (push notifications) ──────────────────────────────────────────────
|
||
gotify:
|
||
image: gotify/server:latest
|
||
restart: unless-stopped
|
||
volumes:
|
||
- gotify_data:/app/data
|
||
expose:
|
||
- "80"
|
||
environment:
|
||
GOTIFY_DEFAULTUSER_NAME: "${GOTIFY_ADMIN_USER}"
|
||
GOTIFY_DEFAULTUSER_PASS: "${GOTIFY_ADMIN_PASS}"
|
||
GOTIFY_SERVER_PORT: "80"
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-sf", "http://localhost:80/health"]
|
||
interval: 15s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
volumes:
|
||
minio_data:
|
||
pb_data:
|
||
meili_data:
|
||
valkey_data:
|
||
caddy_data:
|
||
caddy_config:
|
||
caddy_logs:
|
||
crowdsec_data:
|
||
postgres_data:
|
||
uptime_kuma_data:
|
||
gotify_data:
|