docker-compose.yml
Rama main · réplica de lectura del repositorio original.
# 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