docker-compose.prod.yml
Rama main · réplica de lectura del repositorio original.
# ══════════════════════════════════════════════════════════════════════
# 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/<proj>_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-<id>).
- /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/<user>/...)
# 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 "<KEY> 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 <value>`. 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