# AnanBanana 私有化默认编排(Docker Compose)—— Linux 生产 # ------------------------------------------------------------------ # 两阶段部署语义: # - 联网自举期:首次 `docker compose build/pull` 时从公网拉取基础镜像与 pip 依赖; # - 离线运行期:镜像构建完成后自包含,全部服务在内网/断网环境运行,不再访问公网。 # 所有 image 均钉定具体版本,便于离线预拉取后归档到私有 registry。 # # 用前先: cp deploy/.env.example deploy/.env 并填真实值(GPUStack 地址/密钥、口令、主密钥)。 # 起栈: docker compose --env-file deploy/.env up -d --build # 访问: http:/// (nginx 同源托管前端 + 反代 /api → bff) # ------------------------------------------------------------------ name: ananbanana services: # web:前端静态托管 + BFF 反向代理(浏览器唯一入口,同源)。 web: image: nginx:1.27-alpine volumes: - ../frontend:/usr/share/nginx/html:ro - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro ports: - "${WEB_PORT:-80}:80" depends_on: - bff restart: unless-stopped # BFF:对外编排层(FastAPI)。构建自 backend/,内网暴露 8080(不映射宿主,只经 nginx 访问)。 bff: build: context: ../backend dockerfile: Dockerfile image: ananbanana/bff:0.1.0 # 单进程:多 worker 须先完成状态持久化(P4),否则各进程内存态不一致。 command: ["uvicorn", "bff.app:app", "--host", "0.0.0.0", "--port", "8080"] environment: PYTHONPATH: /app ANANBANANA_ENV: production # 生产模式:禁止静默回退 Mock/演示应答;demo 默认空态 # 控制面鉴权(B1/H3):缺则生产模式所有管理端点 503(fail-closed)。CORS 白名单(H4)默认同源留空。 ANANBANANA_ADMIN_TOKEN: ${ANANBANANA_ADMIN_TOKEN:?set in .env} ANANBANANA_CORS_ORIGINS: ${ANANBANANA_CORS_ORIGINS:-} DATABASE_URL: postgresql://ananbanana:${POSTGRES_PASSWORD:?set in .env}@postgres:5432/ananbanana REDIS_URL: redis://redis:6379/0 GATEWAY_URL: http://gateway:4000 # 真实算力面(必填,否则控制面 503 而非假集群): GPUSTACK_URL: ${GPUSTACK_URL:?set in .env} GPUSTACK_KEY: ${GPUSTACK_KEY:?set in .env} GPUSTACK_API_VERSION: ${GPUSTACK_API_VERSION:-v2} expose: - "8080" depends_on: postgres: condition: service_healthy redis: condition: service_healthy gateway: condition: service_started restart: unless-stopped # gateway:模型网关(LiteLLM),统一 OpenAI 兼容入口,内网 4000。 gateway: image: ghcr.io/berriai/litellm:main-stable command: ["--config", "/app/config.yaml", "--port", "4000"] volumes: - ../backend/gateway/config.yaml:/app/config.yaml:ro environment: DATABASE_URL: postgresql://ananbanana:${POSTGRES_PASSWORD:?set in .env}@postgres:5432/ananbanana # config.yaml 引用的上游/主密钥/租户虚拟 key —— 缺则真实推理链断,必须注入: LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY:?set in .env} GPUSTACK_STRONG_API_BASE: ${GPUSTACK_STRONG_API_BASE:?set in .env} GPUSTACK_WEAK_API_BASE: ${GPUSTACK_WEAK_API_BASE:?set in .env} GPUSTACK_API_KEY: ${GPUSTACK_API_KEY:?set in .env} STRONG_MODEL: ${STRONG_MODEL:-ananbanana-strong} WEAK_MODEL: ${WEAK_MODEL:-ananbanana-weak} VKEY_TENANT_PRO: ${VKEY_TENANT_PRO:?set in .env} VKEY_TENANT_BASIC: ${VKEY_TENANT_BASIC:?set in .env} expose: - "4000" depends_on: postgres: condition: service_healthy restart: unless-stopped # postgres:元数据/审计/密钥存储(仅内网,不映射宿主端口)。 postgres: image: postgres:16-alpine environment: POSTGRES_USER: ananbanana POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set in .env} POSTGRES_DB: ananbanana volumes: - pg_data:/var/lib/postgresql/data expose: - "5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ananbanana"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped # redis:语义缓存(向量)/ 限流 / 告警状态(仅内网)。用 redis-stack 拿到 RediSearch 向量能力。 redis: image: redis/redis-stack-server:latest command: ["redis-stack-server", "--appendonly", "yes"] volumes: - redis_data:/data expose: - "6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped # prometheus:抓取 bff 与 gateway 指标(仅内网;经反代或运维网访问)。 prometheus: image: prom/prometheus:v2.53.0 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - prom_data:/prometheus expose: - "9090" depends_on: - bff - gateway restart: unless-stopped # grafana:可视化面板(默认仅绑本机 3000,凭据从 .env 注入)。 grafana: image: grafana/grafana:11.1.0 environment: GF_SECURITY_ADMIN_USER: ${GRAFANA_USER:-admin} GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:?set in .env} GF_ANALYTICS_REPORTING_ENABLED: "false" GF_ANALYTICS_CHECK_FOR_UPDATES: "false" volumes: - grafana_data:/var/lib/grafana ports: - "127.0.0.1:3000:3000" depends_on: - prometheus restart: unless-stopped volumes: pg_data: redis_data: prom_data: grafana_data: