Ctrl K
ComunidadGuías…

docker-compose.yml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Oneirodex Docker Compose — Unraid / NAS
# App always connects to the Compose service named "db".
#
# ---------------------------------------------------------------------------
# UNRAID VOLUMES (host .env → container mounts)
# Do NOT conflate these two mounts.
#
#   GAMES (scan root only — never uploads)
#     Host:    DATA_FOLDER_GAMES
#     Mount:   → /storage:ro
#     App env: DATA_FOLDER_GAMES=/storage  (container path; hard-set below)
#
#   LIBRARY / UPLOADS (covers, themes, user uploads)
#     Host:    LIBRARY_HOST_PATH
#     Mount:   → /app/oneirodex/static/library  (RW)
#     App env: UPLOAD_FOLDER=/app/oneirodex/static/library  (hard-set below)
#
#   BIOS / FIRMWARE (optional — household Unraid only; never ship binaries in git/image)
#     Host:    EMULATOR_BIOS_HOST_PATH  (appdata, NOT the games share)
#     Mount:   → /app/oneirodex/static/library/bios  (RW; nested under library mount)
#     App env: EMULATOR_BIOS_PATH=/app/oneirodex/static/library/bios
#     Public/GitHub stance: Admin upload only — do not bake firmware into layers.
#     Uncomment the bind below when the host folder has legally obtained files.
#
# Unraid path examples: .env.unraid.example
# Profiles (opt-in): livekit | clamav | challenge
# Observability: commented stub only — see docs/runbooks/observability-profile.md
#
# P3b (ADR 0003): new installs default to oneirodex-* names. Live Unraid stacks
# that still run oneirodex-app / oneirodex-db should pin those names in .env
# until the scan FIFO is idle, then drop the pins so Compose recreates:
#   APP_IMAGE=oneirodex:1.1.9
#   APP_CONTAINER_NAME=oneirodex-app
#   DB_CONTAINER_NAME=oneirodex-db
# Preferred Hub image: cephyrixzyth/oneirodex:1.1.9. Postgres *database* name stays
# POSTGRES_DB (default oneirodex) so existing volumes keep their catalog.
# Sidecar profile container names (livekit / clamav / …) are unchanged.
# ---------------------------------------------------------------------------

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      # Filled by scripts/ops/unraid_ship_update_now.py at deploy time; empty
      # for a hand-run `docker compose build`, which the Ops tile reports as
      # "n/a" rather than guessing.
      args:
        ONEIRODEX_BUILD_SHA: ${ONEIRODEX_BUILD_SHA:-}
        ONEIRODEX_BUILT_AT: ${ONEIRODEX_BUILT_AT:-}
    image: ${APP_IMAGE:-oneirodex:1.1.9}
    container_name: ${APP_CONTAINER_NAME:-oneirodex-app}
    restart: unless-stopped
    ports:
      - "5006:5006"
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-oneirodex}
      - DATABASE_HOST=db
      - DATABASE_PORT=5432
      # Container paths (always); host paths live on the volume mounts below
      - DATA_FOLDER_GAMES=/storage
      # Extra scan locations, as the CONTAINER sees them. Every path listed
      # here needs a matching bind mount below, otherwise it is a root that
      # browses as an empty folder. See docs/runbooks/remote-scan-locations.md
      #   ONEIRODEX_LIBRARY_ROOTS=NAS ROMs=/storage2|Archive=/storage3
      - ONEIRODEX_LIBRARY_ROOTS=${ONEIRODEX_LIBRARY_ROOTS:-}
      - UPLOAD_FOLDER=/app/oneirodex/static/library
      - SECRET_KEY=${SECRET_KEY}
      - DEV_MODE=${DEV_MODE:-false}
      - POSTGRES_USER=${POSTGRES_USER:-postgres}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
      - POSTGRES_DB=${POSTGRES_DB:-oneirodex}
      - ENABLE_ARR_MODULE=${ENABLE_ARR_MODULE:-true}
      - ENABLE_ARR_HARDLINK_PIPELINE=${ENABLE_ARR_HARDLINK_PIPELINE:-true}
      - ALLOW_PRIVATE_LAN_URLS=${ALLOW_PRIVATE_LAN_URLS:-true}
      - ENABLE_AI_ASSIST=${ENABLE_AI_ASSIST:-true}
      - ENABLE_AI_AUTO_APPLY=${ENABLE_AI_AUTO_APPLY:-false}
      # Generated covers (FEAT-D3). Compose has no env_file dump — host .env keys
      # only reach the container when listed here. Point AI_ARTWORK_URL at a LAN
      # GPU box (see docs/runbooks/artwork-gpu-workstation.md); do not rely on
      # the optional `artwork` profile on a GPU-less NAS.
      - ENABLE_AI_ARTWORK=${ENABLE_AI_ARTWORK:-false}
      - AI_ARTWORK_URL=${AI_ARTWORK_URL:-}
      - AI_ARTWORK_ENGINE=${AI_ARTWORK_ENGINE:-a1111}
      - OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434}
      - OLLAMA_MODEL=${OLLAMA_MODEL:-llama3.2}
      - ENABLE_HARDLINK_HELPERS=${ENABLE_HARDLINK_HELPERS:-true}
      - ALLOW_HARDLINK_APPLY=${ALLOW_HARDLINK_APPLY:-false}
      - ENABLE_VR_BROWSE=${ENABLE_VR_BROWSE:-true}
      - OIDC_ENABLED=${OIDC_ENABLED:-false}
      - OIDC_ISSUER_URL=${OIDC_ISSUER_URL:-}
      - OIDC_CLIENT_ID=${OIDC_CLIENT_ID:-}
      - OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET:-}
      - OIDC_REDIRECT_URI=${OIDC_REDIRECT_URI:-}
      - OIDC_SCOPES=${OIDC_SCOPES:-openid email profile}
      - OIDC_ROLE_CLAIM=${OIDC_ROLE_CLAIM:-groups}
      - OIDC_ROLE_MAP=${OIDC_ROLE_MAP:-}
      - OIDC_DISPLAY_NAME=${OIDC_DISPLAY_NAME:-Sign in with SSO}
      - ENABLE_MOD_TRACKING=${ENABLE_MOD_TRACKING:-true}
      - ENABLE_ACTIVITY_FEED=${ENABLE_ACTIVITY_FEED:-true}
      - ENABLE_FREE_GAMES=${ENABLE_FREE_GAMES:-true}
      - ENABLE_DISCOVER_ML=${ENABLE_DISCOVER_ML:-true}
      - ENABLE_EMAIL_DIGEST=${ENABLE_EMAIL_DIGEST:-true}
      - ENABLE_LOGIN_RATE_LIMIT=${ENABLE_LOGIN_RATE_LIMIT:-true}
      - ENABLE_REMOTE_PLAY=${ENABLE_REMOTE_PLAY:-false}
      - ENABLE_AMBIENT_LIGHTING=${ENABLE_AMBIENT_LIGHTING:-false}
      - PROWLARR_URL=${PROWLARR_URL:-}
      - PROWLARR_API_KEY=${PROWLARR_API_KEY:-}
      - JACKETT_URL=${JACKETT_URL:-}
      - JACKETT_API_KEY=${JACKETT_API_KEY:-}
      - QBITTORRENT_URL=${QBITTORRENT_URL:-}
      - QBITTORRENT_USERNAME=${QBITTORRENT_USERNAME:-admin}
      - QBITTORRENT_PASSWORD=${QBITTORRENT_PASSWORD:-}
      # Secure cookies are required by default; set false only for deliberate
      # direct-HTTP LAN/development access (cookies can then cross the network).
      - SESSION_COOKIE_SECURE=${SESSION_COOKIE_SECURE:-true}
      - REMEMBER_COOKIE_SECURE=${REMEMBER_COOKIE_SECURE:-true}
      - CHAT_ATTACHMENT_STORAGE_MAX_BYTES=${CHAT_ATTACHMENT_STORAGE_MAX_BYTES:-1073741824}
      - CHAT_ATTACHMENT_STORAGE_MAX_FILES=${CHAT_ATTACHMENT_STORAGE_MAX_FILES:-10000}
      - TRUSTED_PROXIES=${TRUSTED_PROXIES:-0}
      - TRUSTED_PROXY_CIDRS=${TRUSTED_PROXY_CIDRS:-}
      - ENABLE_LIVEKIT=${ENABLE_LIVEKIT:-true}
      - LIVEKIT_URL=${LIVEKIT_URL:-}
      - LIVEKIT_API_KEY=${LIVEKIT_API_KEY:-}
      - LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET:-}
      - ENABLE_MALWARE_SCAN=${ENABLE_MALWARE_SCAN:-true}
      - MALWARE_SCAN_BLOCK_ON_HIT=${MALWARE_SCAN_BLOCK_ON_HIT:-true}
      - CLAMAV_HOST=${CLAMAV_HOST:-clamav}
      - CLAMAV_PORT=${CLAMAV_PORT:-3310}
      - CLAMAV_SOCKET=${CLAMAV_SOCKET:-}
      - ENABLE_GAME_ASSISTS=${ENABLE_GAME_ASSISTS:-true}
      - ENABLE_DEBRID=${ENABLE_DEBRID:-true}
      - ENABLE_PCDOS_BROWSER=${ENABLE_PCDOS_BROWSER:-true}
      - ENABLE_RUFFLE=${ENABLE_RUFFLE:-true}
      - ENABLE_ROM_PATCH_APPLY=${ENABLE_ROM_PATCH_APPLY:-true}
      - ENABLE_PATCH_CATALOG=${ENABLE_PATCH_CATALOG:-true}
      - ENABLE_ROM_AI_TRANSLATE=${ENABLE_ROM_AI_TRANSLATE:-true}
      - SUPPORT_GITHUB_TOKEN=${SUPPORT_GITHUB_TOKEN:-}
      - SUPPORT_GITHUB_REPO=${SUPPORT_GITHUB_REPO:-cephyrixzyth/oneirodex}
      # Challenge / captcha solver (opt-in — set ENABLE_CHALLENGE_SOLVER=true + URL)
      - ENABLE_CHALLENGE_SOLVER=${ENABLE_CHALLENGE_SOLVER:-false}
      - CHALLENGE_SOLVER_URL=${CHALLENGE_SOLVER_URL:-}
      - CHALLENGE_SOLVER_PROVIDER=${CHALLENGE_SOLVER_PROVIDER:-flaresolverr_compat}
      - CHALLENGE_SOLVER_TIMEOUT_MS=${CHALLENGE_SOLVER_TIMEOUT_MS:-60000}
      - CHALLENGE_SOLVER_MAX_TIER=${CHALLENGE_SOLVER_MAX_TIER:-5}
      # Default 1 — SSE/schedulers single-process; override UVICORN_WORKERS=2 OK
      - UVICORN_WORKERS=${UVICORN_WORKERS:-1}
      # Optional private BIOS mount (container path). Default matches static library bios
      # so WebRetro / Admin see the same folder. Requires config.EMULATOR_BIOS_PATH
      # from env (Backend) when overriding away from the default path.
      - EMULATOR_BIOS_PATH=${EMULATOR_BIOS_PATH:-/app/oneirodex/static/library/bios}
    extra_hosts:
      - "host.docker.internal:host-gateway"
    # ----- UNRAID VOLUMES (see header) -----
    volumes:
      # Games: host DATA_FOLDER_GAMES → scan root only (RO). Never put uploads here.
      - "${DATA_FOLDER_GAMES}:/storage:ro"
      # Library/uploads: host LIBRARY_HOST_PATH → covers, themes, uploads (RW)
      - "${LIBRARY_HOST_PATH:-./data/library}:/app/oneirodex/static/library"
      # Extra scan locations (second disk, NAS share, another appdata share).
      # Uncomment one line per location, mount it RO like /storage, and list the
      # container path in ONEIRODEX_LIBRARY_ROOTS above. The host side is mounted by
      # the host — Docker binds what the kernel already has, it does not speak
      # SMB or NFS itself. See docs/runbooks/remote-scan-locations.md
      # - "${LIBRARY_ROOT_2_HOST_PATH}:/storage2:ro"
      # - "${LIBRARY_ROOT_3_HOST_PATH}:/storage3:ro"
      # - "${LIBRARY_ROOT_4_HOST_PATH}:/storage4:ro"
      # Optional WebRetro cores bind-mount (PCE/VICE/DOS). Host dir must include
      # the shipped 24 cores OR run scripts/fetch-webretro-cores.sh --defaults first —
      # an empty mount hides image cores. See docs/runbooks/webretro-cores.md
      - "${WEBRETRO_CORES_HOST_PATH:-/mnt/cache/appdata/oneirodex/webretro-cores}:/app/oneirodex/static/vendor/webretro/cores"
      # Optional private BIOS / firmware (RW appdata — NOT /storage:ro games).
      # Host example: /mnt/user/appdata/oneirodex/bios
      # Nested bind overrides LIBRARY_HOST_PATH/bios so dumps stay out of the
      # games share. Empty host dir is fine (Admin upload still works into it).
      # Never commit BIOS binaries; never bake them into the public image.
      # - "${EMULATOR_BIOS_HOST_PATH:-/mnt/user/appdata/oneirodex/bios}:/app/oneirodex/static/library/bios"
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      # /awake requires DB + startup init — do not probe login/setup "/"
      test: ["CMD-SHELL", "curl -f http://localhost:5006/awake || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

  db:
    image: postgres:17.6
    container_name: ${DB_CONTAINER_NAME:-oneirodex-db}
    restart: unless-stopped
    shm_size: 128mb
    environment:
      - POSTGRES_USER=${POSTGRES_USER:-postgres}
      # SET A STRONG PASSWORD in .env (.env.*.example say how to generate one).
      # The `postgres` fallback only exists so a stack whose .env predates this
      # variable keeps starting; it is a known password, so never rely on it on a
      # host anyone else can reach. Postgres reads this only when it first creates
      # the db_data volume: changing it later does NOT rotate an existing database
      # (docs/runbooks/docker-compose-deploy.md § Postgres password).
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
      - POSTGRES_DB=${POSTGRES_DB:-oneirodex}
      - POSTGRES_HOST_BIND=${POSTGRES_HOST_BIND:-127.0.0.1}
      - PGDATA=/var/lib/postgresql/data/pgdata
    # Force a Compose-friendly HBA so Docker-bridge IPs can connect without SSL.
    # (Stale volume pg_hba with only hostssl → "no encryption" / no pg_hba entry.)
    # listen_addresses=* is required INSIDE the container: the app reaches db over
    # the Compose bridge network. What keeps it off the LAN is the published-port
    # bind below plus the private-range rules in docker/postgres/pg_hba.conf.
    command:
      - postgres
      - -c
      - listen_addresses=*
      - -c
      - hba_file=/etc/oneirodex/pg_hba.conf
    entrypoint: ["/bin/sh", "/usr/local/bin/oneirodex-postgres-entrypoint.sh"]
    volumes:
      - db_data:/var/lib/postgresql/data/pgdata
      - ./docker/postgres/pg_hba.conf:/etc/oneirodex/pg_hba.conf:ro
      - ./docker/postgres/guard-entrypoint.sh:/usr/local/bin/oneirodex-postgres-entrypoint.sh:ro
      - ./docker/postgres/guard-policy.sh:/usr/local/bin/oneirodex-postgres-guard.sh:ro
    ports:
      # Host access for local pytest / native app against Compose Postgres.
      # Loopback only by default: a Postgres superuser login must not be offered
      # to the whole LAN. To reach it from another machine, opt in explicitly
      # (POSTGRES_HOST_BIND=0.0.0.0, or the host's LAN IP) AND set a strong
      # POSTGRES_PASSWORD. The app container never uses this mapping.
      - "${POSTGRES_HOST_BIND:-127.0.0.1}:${POSTGRES_HOST_PORT:-5432}:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 10s

  # Optional LiveKit SFU — start with: docker compose --profile livekit up -d
  #
  # Not in --dev mode. Dev mode ships the public key pair devkey/secret, so
  # anyone who can reach :7880 could mint room tokens. The pair now comes from
  # LIVEKIT_API_KEY / LIVEKIT_API_SECRET in .env, the same two values the app
  # reads to mint tokens, so there is one source of truth. Generate them
  # (.env.*.example say how) — with either empty LiveKit refuses to start
  # ("one of key-file or keys must be provided") rather than running open.
  # Keep the key and secret alphanumeric (no $ : # or quotes) and the secret at
  # least 32 characters. If you still carry devkey/secret from an older
  # template it starts, but logs "secret is too short" — rotate both.
  #
  # --udp-port 7882 is load-bearing once --dev is gone: dev mode quietly pinned
  # WebRTC media to the single UDP port 7882; without it LiveKit uses UDP
  # 50000-60000, which this file does not publish, and audio would never flow.
  livekit:
    profiles: ['livekit']
    image: livekit/livekit-server:v1.8.4
    container_name: oneirodex-livekit
    restart: unless-stopped
    command: --bind 0.0.0.0 --port 7880 --udp-port 7882
    environment:
      # Must read exactly "key: secret", colon and space included, so the whole
      # entry is quoted for YAML.
      - "LIVEKIT_KEYS=${LIVEKIT_API_KEY:-}: ${LIVEKIT_API_SECRET:-}"
    ports:
      - "${LIVEKIT_HOST_PORT:-7880}:7880"
      - "${LIVEKIT_RTC_PORT:-7881}:7881"
      - "${LIVEKIT_UDP_PORT:-7882}:7882/udp"

  # Optional ClamAV (clamd) — start with: docker compose --profile clamav up -d
  # App uses CLAMAV_HOST=clamav:3310 by default. On Unraid you can instead bind-mount
  # a host clamd socket and set CLAMAV_SOCKET=/run/clamav/clamd.sock (see runbook).
  clamav:
    profiles: ['clamav']
    image: clamav/clamav:1.4
    container_name: oneirodex-clamav
    restart: unless-stopped
    volumes:
      - clamav_db:/var/lib/clamav
    healthcheck:
      test: ['CMD', '/usr/local/bin/clamdcheck.sh']
      interval: 60s
      timeout: 10s
      retries: 5
      start_period: 300s

  # Optional TRAWL challenge solver — start with: docker compose --profile challenge up -d
  # Redis + TRAWL sidecar for FlareSolverr-compatible /v1. No host ports — Docker/LAN only.
  # Set ENABLE_CHALLENGE_SOLVER=true and CHALLENGE_SOLVER_URL=http://trawl:8191 on app.
  # Old NAS / kernel <5.1: TRAWL_IMAGE=ghcr.io/germondai/trawl:baseline — see runbook.
  challenge-redis:
    profiles: ['challenge']
    image: redis:8.10.2-alpine
    container_name: oneirodex-challenge-redis
    restart: unless-stopped
    volumes:
      - challenge_redis_data:/data

  trawl:
    profiles: ['challenge']
    image: ${TRAWL_IMAGE:-ghcr.io/germondai/trawl:latest}
    container_name: oneirodex-trawl
    restart: unless-stopped
    shm_size: 1gb
    environment:
      - REDIS_URL=redis://challenge-redis:6379
      - BROWSER_POOL_SIZE=${TRAWL_BROWSER_POOL_SIZE:-3}
      - MITM_PROXY_ENABLED=${TRAWL_MITM_PROXY_ENABLED:-false}
      - MITM_PROXY_HOST=0.0.0.0
      - MITM_PROXY_PORT=8192
      - MITM_PROXY_CA_DIR=/data/proxy-ca
      - MITM_PROXY_MAX_TIER=${TRAWL_MITM_PROXY_MAX_TIER:-4}
    volumes:
      - trawl_proxy_ca:/data/proxy-ca
    depends_on:
      - challenge-redis
    healthcheck:
      test: ['CMD', 'curl', '-sf', 'http://localhost:8191/health']
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 90s

  # ---------------------------------------------------------------------------
  # profile: artwork — OPTIONAL generated cover art (FEAT-D3)
  #
  # Not started unless you ask for it:  docker compose --profile artwork up -d
  # Then set in .env:  ENABLE_AI_ARTWORK=true
  #                    AI_ARTWORK_URL=http://sdnext:7860
  #
  # SD.Next speaks the same /sdapi/v1/txt2img API as AUTOMATIC1111 and Forge, so
  # AI_ARTWORK_ENGINE=a1111 covers all three. Nothing leaves your network.
  #
  # NOTE: image generation wants a GPU, and this file never asks for one. On CPU
  # it runs and is extremely slow rather than failing, which is the right
  # default: an NVIDIA `deploy` reservation on a host without a loaded driver
  # hard-fails container create ("nvml error: driver not loaded") and takes the
  # whole `compose up` with it. Two ways to get the GPU, neither of them an edit
  # to this file:
  #
  #   GPU in the Docker host   → docker-compose.gpu.yml, via COMPOSE_FILE
  #   GPU on another machine   → docker-compose.artwork-local.yml on that box
  #                              (Windows 2080 workstation: see
  #                              docs/runbooks/artwork-gpu-workstation.md), then
  #                              AI_ARTWORK_URL=http://<that-host>:7860 and do
  #                              not start this profile at all
  # ---------------------------------------------------------------------------
  sdnext:
    profiles: ['artwork']
    image: ${SDNEXT_IMAGE:-saladtechnologies/sdnext:latest}
    container_name: oneirodex-sdnext
    restart: unless-stopped
    environment:
      - SD_NOHASHING=true
    # The image ships a CMD that still passes `--skip-tests`, a flag its own
    # pinned SD.Next checkout (f45b3328, 2023-12-12) never defined — argparse
    # exits before the server binds, so the container restart-loops with
    # "error: unrecognized arguments: --skip-tests". This is the stock CMD
    # minus that flag; `--skip-torch` already covers the torch test it meant.
    command:
      - --listen
      - --no-download
      - --docs
      - --skip-requirements
      - --skip-extensions
      - --skip-git
      - --skip-torch
      - --quick
    volumes:
      # The app lives at /webui with DATA_DIR=/webui/data (models resolve to
      # <data-dir>/models) and runs with cwd=/webui, so generated images land
      # in /webui/outputs. There is no /app in this image.
      - sdnext_models:/webui/data/models
      - sdnext_outputs:/webui/outputs
    ports:
      # SD.Next has no login. Loopback by default so only this host can open its
      # UI; the app reaches it over the Compose network (AI_ARTWORK_URL=
      # http://sdnext:7860) and does not use this mapping. To browse it from
      # another machine set SDNEXT_HOST_BIND to this host's LAN IP.
      - "${SDNEXT_HOST_BIND:-127.0.0.1}:${SDNEXT_HOST_PORT:-7860}:7860"
    # No `deploy:` GPU reservation here on purpose — see the note above. It used
    # to sit commented out at this spot, which invited exactly one fix ("just
    # uncomment it") and no way to undo that per host. It lives in
    # docker-compose.gpu.yml now.
    #
    # The image has no curl (it is a conda-based image; wget is what ships), so
    # a curl healthcheck can only ever fail with "executable file not found" and
    # the container sits unhealthy forever while serving fine.
    healthcheck:
      test: ['CMD', 'wget', '-q', '-O', '/dev/null', 'http://localhost:7860/sdapi/v1/sd-models']
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 180s

  # ---------------------------------------------------------------------------
  # profile: observability — OPTIONAL stub (not enabled; no images required)
  # Prometheus/Grafana are NOT bundled for 1.0. Prefer Admin → Ops + /pulse
  # + /awake. When you add scrape later, uncomment and wire your own config —
  # see docs/runbooks/observability-profile.md. Do not enable until /metrics
  # (or an equivalent scrape target) exists and is auth-gated.
  # ---------------------------------------------------------------------------
  # prometheus:
  #   profiles: ['observability']
  #   image: prom/prometheus:v2.54.1
  #   container_name: oneirodex-prometheus
  #   restart: unless-stopped
  #   volumes:
  #     - ./ops/prometheus.yml:/etc/prometheus/prometheus.yml:ro
  #     - prometheus_data:/prometheus
  #   ports:
  #     - "${PROMETHEUS_HOST_PORT:-9090}:9090"
  # grafana:
  #   profiles: ['observability']
  #   image: grafana/grafana:11.2.0
  #   container_name: oneirodex-grafana
  #   restart: unless-stopped
  #   depends_on: [prometheus]
  #   ports:
  #     - "${GRAFANA_HOST_PORT:-3000}:3000"
  #   volumes:
  #     - grafana_data:/var/lib/grafana

volumes:
  db_data:
    driver: local
  clamav_db:
    driver: local
  challenge_redis_data:
    driver: local
  trawl_proxy_ca:
    driver: local
  sdnext_models:
    driver: local
  sdnext_outputs:
    driver: local