# 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://: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 # /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