# Base compose stack. Uses ${VAR:-default} interpolation throughout so the # stack boots with zero config — sane dev defaults baked in. For production # deployments, override the defaults via shell env vars or a .env file: # # DB_PASSWORD=...real... SECRET_KEY=...real... docker compose up # # The dev override (docker-compose.override.yml) is auto-merged when you # run `docker compose up` from this directory and switches images to # local builds + DEBUG logging. services: redis: image: redis:7-alpine volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_USER: ${DB_USER:-fabledcurator} POSTGRES_PASSWORD: ${DB_PASSWORD:-fabledcurator_dev} POSTGRES_DB: ${DB_NAME:-fabledcurator} volumes: - postgres_data:/var/lib/postgresql/data # Docker's default /dev/shm is 64MB; VACUUM (ANALYZE) + parallel queries # allocate a POSIX dynamic-shared-memory segment in /dev/shm and fail with # "could not resize shared memory segment ... No space left on device" # (operator-flagged 2026-06-07, vacuum_analyze on import_task needed 67MB). # NOTE: a `shm_size:` key is SILENTLY IGNORED under Docker Swarm # (`docker stack deploy` — the prod deployment here), so size /dev/shm via # a tmpfs mount instead — honored by both Swarm and plain Compose. - type: tmpfs target: /dev/shm tmpfs: size: 536870912 # 512 MB healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-fabledcurator}"] interval: 10s timeout: 5s retries: 5 web: image: git.fabledsword.com/bvandeusen/fabledcurator:dev command: ["web"] # Graceful shutdown: give the container time to drain in-flight work on a # deploy (docker SIGTERMs, then SIGKILLs after this window — default is only # 10s, far too short for real jobs). Hypercorn/Celery both warm-shut-down on # SIGTERM; per-lane values sized to typical task length. Anything that still # outruns the window is re-queued (task_reject_on_worker_lost) and re-driven # by the 5-min recovery sweeps, so a kill never corrupts. web = short HTTP # requests + the occasional file download. stop_grace_period: 30s ports: - "${PORT:-8080}:8080" environment: &app_env DB_USER: ${DB_USER:-fabledcurator} DB_PASSWORD: ${DB_PASSWORD:-fabledcurator_dev} DB_HOST: postgres DB_PORT: "5432" DB_NAME: ${DB_NAME:-fabledcurator} CELERY_BROKER_URL: redis://redis:6379/0 CELERY_RESULT_BACKEND: redis://redis:6379/0 SECRET_KEY: ${SECRET_KEY:-dev_secret_key_not_for_production_change_me} EXTENSION_API_KEY: ${EXTENSION_API_KEY:-} LOG_LEVEL: ${LOG_LEVEL:-INFO} volumes: - ./images:/images - ./import:/import # FC-5 legacy migration: bind-mount the host's ImageRepo images dir # under /import (FC's existing filesystem scan picks them up). Read-only # is sufficient — FC copies into /images during the scan. The worker + # scheduler services see the same /import via their own mounts below # because of /import volume reuse. Edit the host path to match your # install before running Settings → Maintenance → Legacy migration. # - /var/lib/imagerepo/images:/import/imagerepo:ro depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } worker: image: git.fabledsword.com/bvandeusen/fabledcurator:dev command: ["worker"] # Drain in-flight import/thumbnail/download tasks before SIGKILL on deploy. stop_grace_period: 90s environment: <<: *app_env CELERY_QUEUES: default,import,thumbnail,download CELERY_CONCURRENCY: "2" # /downloads dropped — nothing in the app references it (operator-flagged # 2026-06-07: it wasn't mapped in prod and everything worked). volumes: - ./images:/images - ./import:/import depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } scheduler: image: git.fabledsword.com/bvandeusen/fabledcurator:dev command: ["scheduler"] # Quick maintenance/scan lane + beat — short tasks, modest drain window. stop_grace_period: 60s environment: <<: *app_env CELERY_QUEUES: maintenance,scan volumes: - ./images:/images - ./import:/import depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } # Dedicated lane for long one-shot maintenance (DB backups, library audits, # admin maintenance). Kept off the scheduler's quick `maintenance` lane so a # 30-min backup or a multi-chunk audit can never starve the 5-min recovery # sweeps / vacuum (operator-flagged 2026-06-07). One slot — these are heavy. maintenance-long: image: git.fabledsword.com/bvandeusen/fabledcurator:dev command: ["worker"] # Longest lane (DB backups, library audits, translation backfill) — give it # the most room to finish a chunk gracefully. Chunked + idempotent, so a job # that still outruns this resumes cleanly next run rather than corrupting. stop_grace_period: 180s environment: <<: *app_env CELERY_QUEUES: maintenance_long CELERY_CONCURRENCY: "1" # Only /images: backups write to /images/_backups, audits read /images, and # the admin tasks (re-extract/cascade-delete/normalize) operate on /images. volumes: - ./images:/images depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } ml-worker: image: git.fabledsword.com/bvandeusen/fabledcurator-ml:dev command: ["ml-worker"] # A single GPU inference pass can run tens of seconds — let it finish. stop_grace_period: 120s environment: <<: *app_env volumes: - ./images:/images:ro - ./models:/models depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } volumes: redis_data: postgres_data: