# ══════════════════════════════════════════════════════════════════════ # Geny — Docker Compose (Production with Nginx / Core — 경량 버전) # # TTS 로컬 엔진 없이 핵심 서비스만 실행합니다. # 도메인을 연결하여 단일 포트(80)로 backend / frontend를 라우팅합니다. # # /api/* → backend:8000 # /health → backend:8000 # /static/assets/* → backend:8000 # /* → frontend:3000 # # Usage: # docker compose -f docker-compose.prod-core.yml up --build -d # docker compose -f docker-compose.prod-core.yml down # docker compose -f docker-compose.prod-core.yml logs -f # # 전체 버전 (TTS 포함): # docker compose -f docker-compose.prod.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-core.yml up --build # ══════════════════════════════════════════════════════════════════════ services: # ── PostgreSQL Database ─────────────────────────────────────────── postgres: image: postgres:16-alpine container_name: geny-postgres-prod 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: 10s timeout: 5s retries: 5 start_period: 10s networks: - geny-net-prod # ── Backend (FastAPI + LangGraph + Claude CLI) ──────────────────── backend: build: context: ./backend dockerfile: Dockerfile container_name: geny-backend-prod 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 # 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} # ── Persistent-storage paths (Phase 1, 2026-05-19) ───────────── # Mirrors docker-compose.prod.yml — each path is backed by a # named volume below. ``-prod`` suffix is shared with the full # prod compose so the two can swap freely without data loss. - 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 volumes: - 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 # ── Backend storage root (notes / vault / capture logs) ──────── - geny-agent-sessions-prod:/data/geny_agent_sessions # ── Phase-1 persistence volumes (2026-05-19) ─────────────────── - 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 # 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 — 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-prod}:/root/.claude:rw depends_on: postgres: condition: service_healthy healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] interval: 30s timeout: 10s retries: 3 start_period: 15s networks: - geny-net-prod # ── 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 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 # ── Avatar Editor (geny-avatar submodule, Next.js) — Prod Core ──── # Same shape as the prod-full entry: nginx in front, no host port, # NEXT_PUBLIC_BASE_PATH=/avatar-editor baked at build time so the # standalone bundle's routes/assets resolve under that prefix. avatar-editor: build: context: ./vendor/geny-avatar dockerfile: Dockerfile args: # NEXT_PUBLIC_* must be in build args (Next.js inlines them at # build time; runtime env is too late for the standalone bundle). 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 # AI provider keys for the editor's Generate panel. Empty # default → provider shows " not set" in dropdown. Set in # .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 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 # ── Nginx Reverse Proxy ─────────────────────────────────────────── nginx: image: nginx:1.27-alpine container_name: geny-nginx-prod 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 networks: geny-net-prod: driver: bridge volumes: geny-pgdata-prod: driver: local geny-tts-cache-prod: driver: local geny-voices-prod: driver: local geny-uploads-prod: driver: local # Shared between avatar-editor (rw) and backend (ro) — prod variant. geny-baked-exports-prod: driver: local # Phase G — Claude Code OAuth credentials. See backend.volumes. geny-claude-creds-prod: driver: local # ── Backend storage root (notes / vault / capture logs) ──────────── # Shares the ``-prod`` suffix with docker-compose.prod.yml so the # two compose files can swap freely without losing state. geny-agent-sessions-prod: driver: local # ── Phase-1 persistent-storage volumes (2026-05-19) ──────────────── 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 geny-mcp-credentials-prod: driver: local