# ══════════════════════════════════════════════════════════════════════ # Geny — Docker Compose (Production with Nginx Reverse Proxy / Full) # # 도메인을 연결하여 단일 포트(80)로 backend / frontend를 라우팅합니다. # 로컬 오디오 엔진 (TTS / STT) 은 --profile audio-local 로 활성화할 수 있습니다. # # /api/* → backend:8000 # /health → backend:8000 # /static/assets/* → backend:8000 # /* → frontend:3000 # # Usage: # docker compose -f docker-compose.prod.yml up --build -d # docker compose -f docker-compose.prod.yml --profile audio-local up --build -d # docker compose -f docker-compose.prod.yml down # docker compose -f docker-compose.prod.yml logs -f # # 경량 버전 (TTS 없음): # docker compose -f docker-compose.prod-core.yml up --build -d # # 도메인 / SSL 설정: # DOMAIN=geny.example.com → nginx server_name 에 반영 # SSL 은 별도의 certbot 또는 Cloudflare 등으로 처리 후 # nginx/nginx.conf 에 443 블록을 추가하세요. # # Port 변경: # NGINX_PORT=58443 docker compose -f docker-compose.prod.yml up --build # # ══════════════════════════════════════════════════════════════════════ services: # ── Qdrant (knowledge repository vector store) ──────────────────── qdrant: # Keep within one minor of the qdrant-client the backend installs # (1.18.x) — mismatch beyond that raises compatibility warnings. image: qdrant/qdrant:v1.18.2 container_name: geny-qdrant-prod restart: unless-stopped volumes: - geny-qdrant-prod:/qdrant/storage networks: - geny-net-prod # ── PostgreSQL Database ─────────────────────────────────────────── postgres: image: postgres:16-alpine container_name: geny-postgres-prod init: true restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB:-geny} POSTGRES_USER: ${POSTGRES_USER:-geny} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-geny123} POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" TZ: ${TIMEZONE:-Asia/Seoul} volumes: - geny-pgdata-prod:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-geny} -d ${POSTGRES_DB:-geny}"] interval: 30s timeout: 5s retries: 3 start_period: 10s networks: - geny-net-prod # ── Backend (FastAPI + LangGraph + Claude CLI) ──────────────────── backend: build: context: ./backend dockerfile: Dockerfile container_name: geny-backend-prod init: true restart: unless-stopped expose: - "8000" env_file: - path: ./backend/.env required: false environment: - APP_HOST=0.0.0.0 - APP_PORT=8000 - GENY_AGENT_STORAGE_ROOT=/data/geny_agent_sessions # ── BLAS threading ──────────────────────────────────────────────── # Belt to main.py's braces. OpenBLAS's worker pool does not survive a # fork, and this process forks constantly (CLI, docker exec, tools); # the next matmul then spins on workers that never come — one core at # 100% inside a microsecond of real work, holding the memory engine's # global lock. That wedged production for 27 hours while /health kept # answering "healthy". No pool, no failure class; these matrices are # far too small for threaded BLAS to pay for its own dispatch. # ── CLI hot-spare ───────────────────────────────────────────── # OFF. 2.65.1 fixed one way a spare could be stale (a reaped child # still reading as alive) and turns ran clean for a while — then the # same signature came back: the CLI starts, lists its tools, exits, # and the turn stalls until the watchdog abandons it at ~320s. # Flipping this to 0 has made the session answer in ~10s every time it # has been tried, so it stays off until the CLI's own exit is # understood. The prewarm buys time-to-first-token, not correctness. - GENY_CLI_PREWARM=${GENY_CLI_PREWARM:-0} - OPENBLAS_NUM_THREADS=1 - OMP_NUM_THREADS=1 - MKL_NUM_THREADS=1 - NUMEXPR_NUM_THREADS=1 # ── Geny Drive (agent cloud storage) ────────────────────────────── # Per-agent workspace quota and per-file cap. Declared explicitly so # the deployed limits are visible/tunable here instead of living as # hidden code defaults — nginx's storage location caps the request # body at 512m; keep MAX_FILE_MB strictly below that (multipart # framing adds overhead, so a file at exactly the nginx cap 413s # before the backend ever sees it — 500 leaves 12m of headroom). - GENY_WORKSPACE_QUOTA_MB=${GENY_WORKSPACE_QUOTA_MB:-10240} - GENY_WORKSPACE_MAX_FILE_MB=${GENY_WORKSPACE_MAX_FILE_MB:-500} # Knowledge repository vector store - GENY_QDRANT_URL=http://qdrant:6333 # PostgreSQL - POSTGRES_HOST=postgres - POSTGRES_PORT=5432 - POSTGRES_DB=${POSTGRES_DB:-geny} - POSTGRES_USER=${POSTGRES_USER:-geny} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-geny123} - AUTO_MIGRATION=true - TIMEZONE=${TIMEZONE:-Asia/Seoul} # Inbound chat gateway (geny-executor >=2.11.0). Set the token in the # root .env to let users DM the VTuber from Telegram; empty = off. - GATEWAY_TELEGRAM_BOT_TOKEN=${GATEWAY_TELEGRAM_BOT_TOKEN:-} - GATEWAY_TELEGRAM_ALLOWED_CHAT_IDS=${GATEWAY_TELEGRAM_ALLOWED_CHAT_IDS:-} - GATEWAY_DISCORD_BOT_TOKEN=${GATEWAY_DISCORD_BOT_TOKEN:-} - GATEWAY_DISCORD_ALLOWED_CHANNEL_IDS=${GATEWAY_DISCORD_ALLOWED_CHANNEL_IDS:-} - GATEWAY_SLACK_APP_TOKEN=${GATEWAY_SLACK_APP_TOKEN:-} - GATEWAY_SLACK_BOT_TOKEN=${GATEWAY_SLACK_BOT_TOKEN:-} - GATEWAY_SLACK_ALLOWED_CHANNEL_IDS=${GATEWAY_SLACK_ALLOWED_CHANNEL_IDS:-} # ── Persistent-storage paths (Phase 1, 2026-05-19) ───────────── # Every path below is backed by a named volume so its data # survives ``docker compose up --build``. Without these env # vars the stores fall back to in-repo defaults that live on # the container's writable layer and get wiped on rebuild. - ENVIRONMENT_STORAGE_PATH=${ENVIRONMENT_STORAGE_PATH:-/data/environments} # Custom MCP server definitions must outlive container recreation — # the legacy in-image path (/app/mcp/custom) was wiped on every # deploy, silently deleting every server registered through the # MCP tab. Rides the existing geny-mcp-credentials-prod volume. - MCP_CUSTOM_STORAGE_PATH=${MCP_CUSTOM_STORAGE_PATH:-/data/mcp/custom} - TRIGGER_PRESET_STORAGE_PATH=${TRIGGER_PRESET_STORAGE_PATH:-/data/trigger_presets} - GENY_TOOL_PRESETS_DIR=/data/tool_presets - GENY_CHAT_CONVERSATIONS_DIR=/data/chat_conversations - GENY_GAME_STATE_DB=/data/creature_state/geny_state.sqlite3 - GENY_CONFIG_VARIABLES_DIR=/data/config_variables - GENY_MCP_CREDENTIALS=/data/mcp/credentials.json # Model-account secrets: API keys, setup tokens, Codex OAuth token # sets. Same named volume as the MCP store, for the same reason — the # container's writable layer is discarded on every rebuild, and losing # this file signs every Claude and Codex account out and erases every # key, with no error until the next turn. - GENY_LLM_SECRETS=/data/mcp/llm_accounts.json # Per-account CLAUDE_CONFIG_DIRs. Each Claude Code login owns one, and # it IS the login — on the container's writable layer every account # would be signed out by a redeploy. - GENY_LLM_ACCOUNTS_ROOT=/data/llm-accounts - GENY_VOICE_STUDIO_DATA_DIR=/data/voice_studio # ── GAPT integration (sandbox/project/deploy delegation) ──────── # geny-backend calls the GAPT control plane over gapt-net and runs # the agent CLI inside GAPT workspace containers via docker exec # (executor ContainerCLIRunner). Empty GAPT_BASE_URL = off (host # execution unchanged). - GAPT_BASE_URL=${GAPT_BASE_URL-http://gapt-server:8088} - GAPT_ADMIN_ID=${GAPT_ADMIN_ID:-admin} - GAPT_ADMIN_PASSWORD=${GAPT_ADMIN_PASSWORD:-admin} # SSO bypass: an authenticated Geny user opens GAPT without a 2nd login # (Geny establishes the GAPT session). Default on; set false to require # GAPT's own login (e.g. when GAPT is exposed independently of Geny). - GENY_GAPT_SSO_BYPASS=${GENY_GAPT_SSO_BYPASS:-true} # Opt-in: run agent sessions inside GAPT workspace sandboxes (sysbox). # Default off → host execution unchanged. Flip to 1 to enable. - GENY_GAPT_WORKSPACES=${GENY_GAPT_WORKSPACES:-0} # One session = one GAPT bind workspace: the HOST path of the # agent-sessions volume (…/docker/volumes/_geny-agent-sessions-…/_data). # Set it (usually in .env) to unify each session's workspace with its # sandbox /workspace; empty keeps the legacy split-filesystem worktree. - GENY_AGENT_SESSIONS_HOST_DIR=${GENY_AGENT_SESSIONS_HOST_DIR:-} - GENY_GAPT_PROJECT_SLUG=${GENY_GAPT_PROJECT_SLUG:-geny} # Sandbox Tool Packs self-service: an agent that builds+saves a pack has it # auto-enabled + auto-opted-into its OWN env, so its future sessions auto- # load it (zero owner steps). Default on; set false for the manual gate. - GENY_PACK_AUTOLOAD_OWN=${GENY_PACK_AUTOLOAD_OWN:-true} # ── geny-avatar integration (Avatar settings category + key sync) ── # Reachable over geny-net-prod; the URL includes the avatar's basePath so # the client appends /api/…. Empty = avatar integration off. - GENY_AVATAR_BASE_URL=${GENY_AVATAR_BASE_URL-http://avatar-editor:3000/avatar-editor} volumes: # Host docker socket — the executor's ContainerCLIRunner shells out # to `docker exec` into GAPT workspace sandboxes (gapt-ws-). - /var/run/docker.sock:/var/run/docker.sock - geny-tts-cache-prod:/app/cache/tts - geny-voices-prod:/app/static/voices # Chat attachment uploads (cycle 20260424_3 PR-1) — without a named # volume here every container restart wiped user-uploaded images. - geny-uploads-prod:/app/static/uploads # Geny agent storage root — holds every session's chat history, # plus the per-user Opsidian vaults (_user_opsidian//...) # with their notes, _attachments/, and _captures.jsonl. Without # this volume the entire backend "memory" — all user notes, all # audio captures, all transcripts — lives on the container's # writable layer and gets wiped on every ``docker compose up # --build backend``. Discovered 2026-05-14 when 13 inbox audio # memos vanished after a routine backend rebuild despite # ``GENY_AGENT_STORAGE_ROOT`` being set in the env block above. - geny-agent-sessions-prod:/data/geny_agent_sessions # ── Phase-1 persistence volumes (2026-05-19) ──────────────────── # Each named volume backs one of the env-var-aware stores # declared in the ``environment:`` block above. Without these # mounts the env vars resolve to paths on the container's # writable layer, which ``docker compose up --build`` wipes. # See store source for the resolver pattern (e.g. backend/ # service/tool_preset/store.py::_resolve_default_dir). - geny-environments-prod:/data/environments - geny-trigger-presets-prod:/data/trigger_presets - geny-tool-presets-prod:/data/tool_presets - geny-chat-conversations-prod:/data/chat_conversations - geny-creature-state-prod:/data/creature_state - geny-config-variables-prod:/data/config_variables - geny-mcp-credentials-prod:/data/mcp - geny-llm-accounts-prod:/data/llm-accounts - geny-voice-studio-prod:/data/voice_studio # Baked puppet zips dropped here by avatar-editor. Backend reads + moves to # installed/ subdir after install (rw — was :ro before C.3 added install). - geny-baked-exports-prod:/data/baked-imports # Phase G of the LLM backend upgrade cycle — Claude Code OAuth # credentials live in /root/.claude/ inside the container. Two # operator paths: # 1) Default (``CLAUDE_HOST_DIR`` unset): use the named volume # ``geny-claude-creds-prod``. Survives container rebuilds. # The user logs in from inside the container via the # Settings → LLM Backends → Claude Code modal ("Sign in # here") — `claude auth login` writes to this volume. # 2) Override (``CLAUDE_HOST_DIR=/abs/path``): bind-mount a # host directory. Use this when the operator has already # run `claude auth login` on the host and wants the # container to share that subscription. # The earlier ``${HOME}/.claude`` default was sudo-broken — under # ``sudo docker compose`` HOME=/root, so the bind would silently # create an empty /root/.claude on the host. Named volume avoids # that footgun entirely. - ${CLAUDE_HOST_DIR:-geny-claude-creds-prod}:/root/.claude:rw depends_on: postgres: condition: service_healthy healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:8000/health"] # 15s interval + 3 retries → a wedge is flagged unhealthy in <1min, then # the autoheal sidecar restarts it. (Was 30s; tightened after a session # memory-load froze the event loop and the container sat dead for hours.) interval: 15s timeout: 5s retries: 3 start_period: 40s # Let py-spy / gdb attach to the running interpreter for live diagnosis of # an event-loop stall (a hung task can't be sampled otherwise). Inert # during normal operation. cap_add: - SYS_PTRACE labels: # Watched by the `autoheal` sidecar below: when the healthcheck goes # unhealthy the container is auto-restarted instead of sitting dead — # turns a multi-hour outage into a ~30s blip. - autoheal=true networks: - geny-net-prod # Reach the GAPT control plane (gapt-server:8088) + workspace # sandbox containers for the bridged MCP callback. - gapt-net # ── Autoheal (watchdog) ─────────────────────────────────────────── # Monitors containers labelled ``autoheal=true`` and restarts any that Docker # reports ``unhealthy``. Docker's own ``restart: unless-stopped`` only fires # on process EXIT — a process that is alive but WEDGED (event loop blocked on # a slow/stuck syscall) never exits, so it would otherwise stay dead until a # human intervenes. This closes that gap. autoheal: image: willfarrell/autoheal:1.2.0 container_name: geny-autoheal-prod restart: unless-stopped environment: - AUTOHEAL_CONTAINER_LABEL=autoheal - AUTOHEAL_INTERVAL=10 # check every 10s - AUTOHEAL_START_PERIOD=45 # grace after a (re)start before watching - AUTOHEAL_DEFAULT_STOP_TIMEOUT=40 # SIGTERM grace before SIGKILL (a # wedged proc in D-state may need the # kill; 40s lets in-flight I/O drain) - TZ=${TIMEZONE:-Asia/Seoul} volumes: - /var/run/docker.sock:/var/run/docker.sock # ── Frontend (Next.js) ──────────────────────────────────────────── frontend: build: # Root context: the page renders shared/chat, which lives beside # frontend/ and must be in the image. context: . dockerfile: frontend/Dockerfile args: - API_URL=http://backend:8000 - NEXT_PUBLIC_API_URL= container_name: geny-frontend-prod init: true restart: unless-stopped expose: - "3000" environment: - API_URL=http://backend:8000 - NEXT_PUBLIC_API_URL= - PORT=3000 - HOSTNAME=0.0.0.0 depends_on: backend: condition: service_healthy networks: - geny-net-prod # ── Nginx Reverse Proxy ─────────────────────────────────────────── nginx: image: nginx:1.27-alpine container_name: geny-nginx-prod init: true restart: unless-stopped ports: - "${NGINX_PORT:-58443}:80" volumes: - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro depends_on: frontend: condition: service_started backend: condition: service_healthy avatar-editor: condition: service_started networks: - geny-net-prod # ── OmniVoice (Optional — Geny 자체 TTS 마이크로서비스, GPU 필요) ─ # geny-monorepo 내 omnivoice/ 에서 빌드. 외부 노출 없이 backend 가 # http://omnivoice:9881 로 호출. HF 모델 캐시는 영구 볼륨에. omnivoice: build: context: ./omnivoice dockerfile: Dockerfile image: geny-omnivoice:prod container_name: geny-omnivoice-prod profiles: ["audio-local"] init: true restart: unless-stopped expose: - "9881" environment: - OMNIVOICE_MODEL=${OMNIVOICE_MODEL:-k2-fsa/OmniVoice} - OMNIVOICE_DEVICE=${OMNIVOICE_DEVICE:-cuda:0} - OMNIVOICE_DTYPE=${OMNIVOICE_DTYPE:-float16} # GPU 동시 합성 슬롯 (asyncio.Semaphore). 2026-04 RTX 5070 # (Blackwell, sm_120, 12GB, Tensor cores) 로 업그레이드하며 4 로 상향. # 더 작은 GPU 또는 공유 호스트에서는 1~2 로 내릴 것. - OMNIVOICE_MAX_CONCURRENCY=${OMNIVOICE_MAX_CONCURRENCY:-1} # Diffusion outer-loop step 수. 원본 기본값 32. 16 = 품질/속도 균형 # (5070 측정 기준 RTF 충분히 안정). 음질 우선이면 24, 속도 우선이면 # 8~12 까지 내릴 수 있음. - OMNIVOICE_DEFAULT_NUM_STEP=${OMNIVOICE_DEFAULT_NUM_STEP:-24} # CUDA 컨텍스트 메모리 단편화 방지. 0.85 = 12GB 5070 에서 ~10.2GB # 예약 (전용 GPU 호스트 기준). 공유 시 0.5~0.7 로 낮춰서 운영. # 0 으로 두면 비활성화. # 2026-05-29: bumped from 0.5 → 0.65 after observing post-reboot # idle GPU usage of ~1.7 GiB (other containers) on the 12 GiB # 5070. 0.65 → 7.48 GiB cap; combined with neighbours peaks ~9.2 # GiB (~80 % of total), leaves the OS comfortable headroom. - OMNIVOICE_GPU_MEMORY_FRACTION=${OMNIVOICE_GPU_MEMORY_FRACTION:-0.65} # CUDA caching-allocator: switch to expandable segments to avoid the # fragmentation-induced OOM the production GPU was hitting at the # 50% fraction cap (5.74 / 5.75 GiB used, no contiguous block left # for a 20 MiB allocation). See pytorch.org/docs/stable/notes/ # cuda.html#environment-variables. - PYTORCH_CUDA_ALLOC_CONF=${PYTORCH_CUDA_ALLOC_CONF:-expandable_segments:True} - OMNIVOICE_AUTO_ASR=${OMNIVOICE_AUTO_ASR:-false} - OMNIVOICE_LOG_LEVEL=${OMNIVOICE_LOG_LEVEL:-info} - HF_ENDPOINT=${HF_ENDPOINT:-https://huggingface.co} volumes: - geny-voices-prod:/voices:ro - geny-omnivoice-models-prod:/models # Output-equivalence baselines persist across container restarts so # every Tier-A PR can be diffed against the same reference PCM. - ./omnivoice/baselines:/baselines shm_size: 8G deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost:9881/health"] interval: 30s timeout: 10s retries: 3 start_period: 300s networks: - geny-net-prod # ── Avatar Editor (geny-avatar submodule, Next.js) — Prod ───────── # In prod, nginx terminates external traffic and proxies the # /avatar-editor/ prefix to this service over the geny-net-prod # network. avatar-editor is therefore internal-only (no host port) # and built with NEXT_PUBLIC_BASE_PATH=/avatar-editor so all routes, # static assets, and apiUrl() fetches resolve under that prefix. avatar-editor: build: context: ./vendor/geny-avatar dockerfile: Dockerfile args: # NEXT_PUBLIC_* are inlined into the JS bundle by next build — # they MUST be passed as build args, not just runtime env, or # the bundle ships with empty values. basePath drives routing, # GENY_HOST gates the "send to Geny" button + /api/send-to-geny # route in the avatar-editor app. NEXT_PUBLIC_BASE_PATH: /avatar-editor NEXT_PUBLIC_GENY_HOST: "true" container_name: geny-avatar-editor-prod init: true restart: unless-stopped expose: - "3000" environment: - NEXT_PUBLIC_BASE_PATH=/avatar-editor - NEXT_PUBLIC_GENY_HOST=true - GENY_BAKED_EXPORTS_DIR=/exports # API 설정 모달이 쓰는 config.json — 전용 볼륨에 두어 이미지 # 재빌드/컨테이너 재생성에도 키 설정이 유지되게 한다. - GENY_AVATAR_CONFIG_PATH=/config/config.json # AI provider keys consumed by the editor's Generate panel API # routes (lib/ai/providers/registry.ts). Empty default means the # provider shows up as " not set" in the dropdown without # crashing. Set in your .env to enable. - GEMINI_API_KEY=${GEMINI_API_KEY:-} - OPENAI_API_KEY=${OPENAI_API_KEY:-} - REPLICATE_API_TOKEN=${REPLICATE_API_TOKEN:-} - PORT=3000 - HOSTNAME=0.0.0.0 - NODE_ENV=production volumes: - geny-baked-exports-prod:/exports - geny-avatar-config-prod:/config healthcheck: test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3000/avatar-editor/ || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 30s networks: - geny-net-prod # ── Whisper STT (Optional — vLLM 기반 음성 전사, GPU 필요) ──────── # geny-monorepo 내 whisper-stt/ 에서 빌드. 외부 노출 없이 backend 가 # http://whisper-stt:8001 로 호출. vLLM 의 OpenAI 호환 # /v1/audio/transcriptions endpoint 를 그대로 사용. # # GPU 분할 (RTX 5070 12GB): # omnivoice → 0.50 (≈ 6 GB) # whisper → 0.35 (≈ 4 GB) # ──────────────────── # 합계 0.85 (≈ 10 GB) ← 15% 여유로 OOM 방지 whisper-stt: build: context: ./whisper-stt dockerfile: Dockerfile image: geny-whisper-stt:prod container_name: geny-whisper-stt-prod profiles: ["audio-local"] init: true restart: unless-stopped expose: - "8001" environment: - WHISPER_MODEL=${WHISPER_MODEL:-openai/whisper-large-v3} - WHISPER_PORT=${WHISPER_PORT:-8001} # vLLM 의 GPU 메모리 점유율. OmniVoice 와 같은 GPU 위에서 # 공존 가능하도록 보수적 분할. - WHISPER_GPU_MEMORY_FRACTION=${WHISPER_GPU_MEMORY_FRACTION:-0.35} - WHISPER_DTYPE=${WHISPER_DTYPE:-auto} # FlashAttention 2 stalls on Blackwell (sm_120, RTX 50 series) # on the encoder-decoder Whisper path — observed as a 24+ min # hang right after "Using FLASH_ATTN attention backend". The # entrypoint passes this through as `vllm serve # --attention-backend `. Default TRITON_ATTN since # Triton compiles its kernel JIT and doesn't need a # precompiled FA2 binary for sm_120. Switch to FLASH_ATTN / # FLASHINFER once vllm ships Blackwell-ready binaries. - WHISPER_ATTENTION_BACKEND=${WHISPER_ATTENTION_BACKEND:-TRITON_ATTN} # HuggingFace 모델 다운로드 (public 모델이라 토큰 없이 가능, # rate-limit 회피용으로만 옵션 제공). - HUGGINGFACE_HUB_TOKEN=${HUGGINGFACE_HUB_TOKEN:-} - HF_ENDPOINT=${HF_ENDPOINT:-https://huggingface.co} volumes: - geny-whisper-cache-prod:/cache/huggingface shm_size: 4G deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost:8001/health"] interval: 30s timeout: 10s retries: 5 # 첫 부팅: 모델 다운로드 (~3 GB) + vLLM 로드. 이후 부팅은 # 캐시 hit 으로 ~30 s. 충분히 크게. start_period: 600s networks: - geny-net-prod networks: geny-net-prod: driver: bridge # Shared with the GAPT stack (deploy/gapt + gapt/compose). GAPT's # compose creates it (name: gapt-net); Geny joins it as external so # geny-backend can reach gapt-server + workspace sandboxes. gapt-net: external: true volumes: geny-qdrant-prod: geny-pgdata-prod: driver: local # avatar-editor 의 런타임 config.json (API 키 설정 모달) geny-avatar-config-prod: driver: local geny-tts-cache-prod: driver: local geny-voices-prod: driver: local geny-uploads-prod: driver: local # Holds the backend storage root (``GENY_AGENT_STORAGE_ROOT`` → # ``/data/geny_agent_sessions``). Includes every session's chat # log, per-user Opsidian vault (notes + attachments + capture # logs), and the agent memory provider's file backend. Mandatory # for cross-rebuild persistence — see backend.volumes comment. geny-agent-sessions-prod: driver: local geny-omnivoice-models-prod: driver: local # Whisper 모델 가중치 영구 캐시. 첫 부팅 시 ~3 GB 다운로드. geny-whisper-cache-prod: driver: local # Shared between avatar-editor (rw) and backend (ro) — prod variant. geny-baked-exports-prod: driver: local # Phase G — backend's /root/.claude (Claude Code OAuth credentials) # when CLAUDE_HOST_DIR isn't overridden. See backend.volumes for the # full rationale. geny-claude-creds-prod: driver: local # ── Phase-1 persistent-storage volumes (2026-05-19) ───────────────── # Backs the env-var-aware stores. See backend.environment for the # env-var mapping and backend.volumes for the mount points. Without # these named volumes, every ``docker compose up --build`` wipes the # environment configs, tool presets, chat history, creature state, # MCP OAuth tokens, and per-template variable overrides. geny-environments-prod: driver: local geny-trigger-presets-prod: driver: local geny-tool-presets-prod: driver: local geny-chat-conversations-prod: driver: local geny-creature-state-prod: driver: local geny-config-variables-prod: driver: local # Per-account Claude Code credential directories. This volume IS the set of # logins — losing it means signing every account in again by hand. geny-llm-accounts-prod: geny-mcp-credentials-prod: geny-voice-studio-prod: driver: local