# ══════════════════════════════════════════════════════════════════════ # Geny — Docker Compose (Standalone / Full) # # 전체 서비스 포함 (로컬 오디오 엔진 TTS/STT 는 --profile audio-local 로 활성화) # # Usage: # docker compose up --build # 기본 실행 (TTS 미포함) # docker compose up -d # 백그라운드 실행 # docker compose --profile audio-local up # 로컬 오디오 엔진 (TTS + STT) 포함 # docker compose down # 종료 # docker compose logs -f # 로그 확인 # # Port 변경: # BACKEND_PORT=8080 FRONTEND_PORT=3001 docker compose up --build # # 또는 .env 파일로 설정: # BACKEND_PORT=8080 # FRONTEND_PORT=3001 # # ══════════════════════════════════════════════════════════════════════ services: # ── PostgreSQL Database ─────────────────────────────────────────── postgres: image: postgres:16-alpine container_name: geny-postgres init: true restart: unless-stopped ports: - "127.0.0.1:${POSTGRES_PORT:-5432}:5432" 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:/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 # ── Backend (FastAPI + LangGraph + Claude CLI) ──────────────────── backend: build: context: ./backend dockerfile: Dockerfile container_name: geny-backend init: true restart: unless-stopped ports: - "127.0.0.1:${BACKEND_PORT:-8000}: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 # 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 .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) ───────────── # Mirrors docker-compose.prod.yml — each path is backed by a # named volume so its data survives ``docker compose up --build``. - ENVIRONMENT_STORAGE_PATH=${ENVIRONMENT_STORAGE_PATH:-/data/environments} - 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 - GENY_VOICE_STUDIO_DATA_DIR=/data/voice_studio volumes: - geny-tts-cache:/app/cache/tts - geny-voices:/app/static/voices # ── Backend storage root (notes / vault / capture logs) ──────── - geny-agent-sessions:/data/geny_agent_sessions # ── Phase-1 persistence volumes (2026-05-19) ─────────────────── - geny-environments:/data/environments - geny-trigger-presets:/data/trigger_presets - geny-tool-presets:/data/tool_presets - geny-chat-conversations:/data/chat_conversations - geny-creature-state:/data/creature_state - geny-config-variables:/data/config_variables - geny-mcp-credentials:/data/mcp - geny-voice-studio:/data/voice_studio # Chat attachment uploads (content-addressable: /app/static/uploads/{shard}/{sha256}.{ext}). # Cycle 20260424_3 PR-1: without this volume the folder was ephemeral — # every container restart wiped user-uploaded images. - geny-uploads:/app/static/uploads # Baked puppet zips dropped here by the avatar-editor service. Backend # lists pending zips and "installs" them into static/{live2d,spine}-models/, # then moves the consumed zip into installed/ — rw mount required. # See docs/plan/GENY_AVATAR_INTEGRATION.md (Phase C). - geny-baked-exports:/data/baked-imports # Phase G — host's ``~/.claude`` for Pro/Max OAuth credential # passthrough to the in-container ``claude`` CLI. RW for token # refresh. - ${CLAUDE_HOST_DIR:-geny-claude-creds}:/root/.claude:rw depends_on: postgres: condition: service_healthy healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:8000/health"] interval: 30s timeout: 5s retries: 3 start_period: 15s networks: - geny-net # ── 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 container_name: geny-frontend init: true restart: unless-stopped ports: - "127.0.0.1:${FRONTEND_PORT:-3000}:3000" environment: - API_URL=http://backend:8000 - PORT=3000 - HOSTNAME=0.0.0.0 depends_on: backend: condition: service_healthy networks: - geny-net # ── OmniVoice (Optional — Geny 자체 TTS 마이크로서비스, GPU 필요) ─ # geny-monorepo 내 omnivoice/ 에서 빌드. 포트 9881, voices 공유 볼륨, # HF 모델 캐시는 별도 영구 볼륨. profile: audio-local 로 활성화. omnivoice: build: context: ./omnivoice dockerfile: Dockerfile image: geny-omnivoice:latest container_name: geny-omnivoice profiles: ["audio-local"] init: true restart: unless-stopped ports: - "127.0.0.1:${OMNIVOICE_PORT:-9881}:9881" environment: - OMNIVOICE_MODEL=${OMNIVOICE_MODEL:-k2-fsa/OmniVoice} - OMNIVOICE_DEVICE=${OMNIVOICE_DEVICE:-cuda:0} - OMNIVOICE_DTYPE=${OMNIVOICE_DTYPE:-float16} # 5070(Blackwell, sm_120) 기준. 공유 GPU 환경에서는 1~2 로 내릴 것. - OMNIVOICE_MAX_CONCURRENCY=${OMNIVOICE_MAX_CONCURRENCY:-4} - OMNIVOICE_DEFAULT_NUM_STEP=${OMNIVOICE_DEFAULT_NUM_STEP:-16} - OMNIVOICE_GPU_MEMORY_FRACTION=${OMNIVOICE_GPU_MEMORY_FRACTION:-0.85} - 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:/voices:ro - geny-omnivoice-models:/models 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 # ── Avatar Editor (geny-avatar submodule, Next.js) ──────────────── # Standalone Next.js app from vendor/geny-avatar (pinned to v0.2.0). # Lets users edit Spine/Cubism puppets in-browser and "Send to Geny" # baked model zips, which land in the geny-baked-exports volume the # backend reads from. Standalone compose has no nginx in front, so # the editor binds directly to a host port (default 3001). # # Build context is the submodule path — fresh clones must run # `git submodule update --init --recursive` first or the build fails. avatar-editor: build: context: ./vendor/geny-avatar dockerfile: Dockerfile args: # NEXT_PUBLIC_* are inlined at build time; runtime env below is # too late. basePath stays empty (root mount, no nginx); GENY_HOST # enables the "send to Geny" button + /api/send-to-geny route. NEXT_PUBLIC_BASE_PATH: "" NEXT_PUBLIC_GENY_HOST: "true" container_name: geny-avatar-editor init: true restart: unless-stopped ports: - "127.0.0.1:${AVATAR_EDITOR_PORT:-3001}:3000" environment: # Standalone compose has no reverse proxy → avatar-editor served # at root, so basePath stays empty (Next.js falls back to "/" routes). - NEXT_PUBLIC_BASE_PATH= # Enables Geny library auto-sync (lib/sync/genySync) + the # /api/library/sync proxy route. Browser-visible flag. - NEXT_PUBLIC_GENY_HOST=true - GENY_BAKED_EXPORTS_DIR=/exports # AI provider keys for the editor's Generate panel. - 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: # Write side of the shared baked-zip handoff (backend mounts the # same volume read-only at /data/baked-imports). - geny-baked-exports:/exports healthcheck: test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3000/ || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 30s networks: - geny-net networks: geny-net: driver: bridge volumes: geny-pgdata: driver: local geny-tts-cache: driver: local geny-voices: driver: local geny-uploads: driver: local geny-omnivoice-models: driver: local # Shared between avatar-editor (rw at /exports) and backend (ro at # /data/baked-imports). Carries baked puppet zips from the editor's # "send to Geny" action to the backend's import flow. geny-baked-exports: driver: local # Phase G — Claude Code OAuth credentials. See backend.volumes. geny-claude-creds: driver: local # ── Backend storage root (notes / vault / capture logs) ─────────── geny-agent-sessions: driver: local # ── Phase-1 persistent-storage volumes (2026-05-19) ─────────────── geny-environments: driver: local geny-trigger-presets: driver: local geny-tool-presets: driver: local geny-chat-conversations: driver: local geny-creature-state: driver: local geny-config-variables: driver: local geny-mcp-credentials: geny-voice-studio: driver: local