Conflicts resolved (preserving fork customizations): - config.go: keep NodeTLSProxy + add upstream OpenAIHTTP2 - gateway_service.go: NewGatewayService now takes both rpmTokenBucketSvc (local) and userPlatformQuotaRepo (upstream) - wire_gen.go: wire both new args into the call site - http_upstream.go: drop redundant settings re-assignment; keep proxy URL log redaction - http_upstream_test.go: adopt upstream's explicit-0-disables semantics; keep 600s default constant in nil-cfg fallback test - user_handler_test.go / gateway_record_usage_test.go: pick up new userPlatformQuotaRepo nil parameter Also updated test stubs (windsurf_google_login_test.go, windsurf_tier_access_service_test.go, gateway_models_test.go) for new SetModelRateLimit variadic signature and the extra NewGatewayService arg. Upstream highlights: OpenAI embeddings gateway, user x platform USD quota, content-moderation risk thresholds, OAuth 401 credentials no-overwrite fix, HTTP/2 OpenAI upstream config, pool retry status code configurability, long-context cache pricing multipliers.
271 lines
11 KiB
YAML
271 lines
11 KiB
YAML
# =============================================================================
|
||
# Sub2API - Docker Compose Configuration (All-in-One)
|
||
# =============================================================================
|
||
# 包含服务:
|
||
# - sub2api 主应用
|
||
# - postgres 数据库
|
||
# - redis 缓存
|
||
# - windsurf-ls 可选: Windsurf Language Server (通过 profile 启用)
|
||
#
|
||
# 启动方式:
|
||
# 默认三件套: docker compose up -d
|
||
# 含 Windsurf LS: docker compose --profile windsurf up -d
|
||
#
|
||
# 注意:
|
||
# - 首次启动前先复制 .env.example 为 .env 并填写必填变量
|
||
# - JWT_SECRET / TOTP_ENCRYPTION_KEY 多实例必须固定
|
||
# - windsurf-ls 镜像仅支持 linux/amd64, arm64 宿主机会通过 QEMU 模拟
|
||
# =============================================================================
|
||
|
||
services:
|
||
# ===========================================================================
|
||
# Sub2API Application
|
||
# ===========================================================================
|
||
sub2api:
|
||
image: docker.io/zfc931912343/sub2api:latest
|
||
container_name: sub2api
|
||
restart: unless-stopped
|
||
ulimits:
|
||
nofile:
|
||
soft: 100000
|
||
hard: 100000
|
||
ports:
|
||
- "0.0.0.0:80:8080"
|
||
volumes:
|
||
- sub2api_data:/app/data
|
||
# Optional: 挂载自定义 config.yaml(先从 config.example.yaml 复制并修改)
|
||
# - ./config.yaml:/app/data/config.yaml
|
||
# Optional: 自定义 Codex instructions 模板
|
||
# - ./codex-instructions.md.tmpl:/app/data/codex-instructions.md.tmpl:ro
|
||
environment:
|
||
- AUTO_SETUP=true
|
||
|
||
# --- Server ---
|
||
- SERVER_HOST=0.0.0.0
|
||
- SERVER_PORT=8080
|
||
- SERVER_MODE=${SERVER_MODE:-release}
|
||
- RUN_MODE=${RUN_MODE:-standard}
|
||
|
||
# --- Database (PostgreSQL) ---
|
||
- DATABASE_HOST=postgres
|
||
- DATABASE_PORT=5432
|
||
- DATABASE_USER=${POSTGRES_USER:-sub2api}
|
||
- DATABASE_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
|
||
- DATABASE_DBNAME=${POSTGRES_DB:-sub2api}
|
||
- DATABASE_SSLMODE=disable
|
||
- DATABASE_MAX_OPEN_CONNS=${DATABASE_MAX_OPEN_CONNS:-50}
|
||
- DATABASE_MAX_IDLE_CONNS=${DATABASE_MAX_IDLE_CONNS:-10}
|
||
- DATABASE_CONN_MAX_LIFETIME_MINUTES=${DATABASE_CONN_MAX_LIFETIME_MINUTES:-30}
|
||
- DATABASE_CONN_MAX_IDLE_TIME_MINUTES=${DATABASE_CONN_MAX_IDLE_TIME_MINUTES:-5}
|
||
|
||
# --- Redis ---
|
||
- REDIS_HOST=redis
|
||
- REDIS_PORT=6379
|
||
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
||
- REDIS_DB=${REDIS_DB:-0}
|
||
- REDIS_POOL_SIZE=${REDIS_POOL_SIZE:-1024}
|
||
- REDIS_MIN_IDLE_CONNS=${REDIS_MIN_IDLE_CONNS:-10}
|
||
- REDIS_ENABLE_TLS=${REDIS_ENABLE_TLS:-false}
|
||
|
||
# --- Admin(仅首次启动生效)---
|
||
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local}
|
||
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
|
||
|
||
# --- JWT(多实例必须固定,否则重启后 session 失效)---
|
||
# 生成: openssl rand -hex 32
|
||
- JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required for multi-instance}
|
||
- JWT_EXPIRE_HOUR=${JWT_EXPIRE_HOUR:-24}
|
||
|
||
# --- TOTP 2FA(多实例必须固定,否则 2FA 失效)---
|
||
# 生成: openssl rand -hex 32
|
||
- TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY:?TOTP_ENCRYPTION_KEY is required for multi-instance}
|
||
|
||
# --- Timezone ---
|
||
- TZ=${TZ:-Asia/Shanghai}
|
||
|
||
# --- Gemini OAuth ---
|
||
- GEMINI_OAUTH_CLIENT_ID=${GEMINI_OAUTH_CLIENT_ID:-}
|
||
- GEMINI_OAUTH_CLIENT_SECRET=${GEMINI_OAUTH_CLIENT_SECRET:-}
|
||
- GEMINI_OAUTH_SCOPES=${GEMINI_OAUTH_SCOPES:-}
|
||
- GEMINI_QUOTA_POLICY=${GEMINI_QUOTA_POLICY:-}
|
||
- GEMINI_CLI_OAUTH_CLIENT_SECRET=${GEMINI_CLI_OAUTH_CLIENT_SECRET:-}
|
||
- ANTIGRAVITY_OAUTH_CLIENT_SECRET=${ANTIGRAVITY_OAUTH_CLIENT_SECRET:-}
|
||
- ANTIGRAVITY_USER_AGENT_VERSION=${ANTIGRAVITY_USER_AGENT_VERSION:-}
|
||
|
||
# --- Security ---
|
||
- SECURITY_URL_ALLOWLIST_ENABLED=${SECURITY_URL_ALLOWLIST_ENABLED:-false}
|
||
- SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=${SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP:-false}
|
||
- SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-false}
|
||
- SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS=${SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS:-}
|
||
|
||
# --- Update Proxy(国内机器可配置代理访问 GitHub)---
|
||
- UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-}
|
||
|
||
# --- Windsurf (账号管理/登录,不依赖 LS) ---
|
||
- WINDSURF_ENABLED=${WINDSURF_ENABLED:-false}
|
||
- WINDSURF_FIREBASE_API_KEY=${WINDSURF_FIREBASE_API_KEY:-}
|
||
|
||
# --- Windsurf Language Server (可选,启用 windsurf profile 时生效) ---
|
||
- WINDSURF_DOCKER_HOST=${WINDSURF_DOCKER_HOST:-windsurf-ls}
|
||
- WINDSURF_DOCKER_PORT=${WINDSURF_DOCKER_PORT:-42099}
|
||
- WINDSURF_DOCKER_CSRF_TOKEN=${WINDSURF_DOCKER_CSRF_TOKEN:-}
|
||
|
||
# =======================================================================
|
||
# Image Generation Stream & Concurrency
|
||
# =======================================================================
|
||
# OpenAI HTTP upstream protocol/timeout
|
||
- GATEWAY_OPENAI_RESPONSE_HEADER_TIMEOUT=${GATEWAY_OPENAI_RESPONSE_HEADER_TIMEOUT:-0}
|
||
- GATEWAY_OPENAI_HTTP2_ENABLED=${GATEWAY_OPENAI_HTTP2_ENABLED:-true}
|
||
- GATEWAY_OPENAI_HTTP2_ALLOW_PROXY_FALLBACK_TO_HTTP1=${GATEWAY_OPENAI_HTTP2_ALLOW_PROXY_FALLBACK_TO_HTTP1:-true}
|
||
- GATEWAY_OPENAI_HTTP2_FALLBACK_ERROR_THRESHOLD=${GATEWAY_OPENAI_HTTP2_FALLBACK_ERROR_THRESHOLD:-2}
|
||
- GATEWAY_OPENAI_HTTP2_FALLBACK_WINDOW_SECONDS=${GATEWAY_OPENAI_HTTP2_FALLBACK_WINDOW_SECONDS:-60}
|
||
- GATEWAY_OPENAI_HTTP2_FALLBACK_TTL_SECONDS=${GATEWAY_OPENAI_HTTP2_FALLBACK_TTL_SECONDS:-600}
|
||
- GATEWAY_IMAGE_STREAM_DATA_INTERVAL_TIMEOUT=${GATEWAY_IMAGE_STREAM_DATA_INTERVAL_TIMEOUT:-900}
|
||
- GATEWAY_IMAGE_STREAM_KEEPALIVE_INTERVAL=${GATEWAY_IMAGE_STREAM_KEEPALIVE_INTERVAL:-10}
|
||
- GATEWAY_IMAGE_CONCURRENCY_ENABLED=${GATEWAY_IMAGE_CONCURRENCY_ENABLED:-false}
|
||
- GATEWAY_IMAGE_CONCURRENCY_MAX_CONCURRENT_REQUESTS=${GATEWAY_IMAGE_CONCURRENCY_MAX_CONCURRENT_REQUESTS:-0}
|
||
- GATEWAY_IMAGE_CONCURRENCY_OVERFLOW_MODE=${GATEWAY_IMAGE_CONCURRENCY_OVERFLOW_MODE:-reject}
|
||
- GATEWAY_IMAGE_CONCURRENCY_WAIT_TIMEOUT_SECONDS=${GATEWAY_IMAGE_CONCURRENCY_WAIT_TIMEOUT_SECONDS:-30}
|
||
- GATEWAY_IMAGE_CONCURRENCY_MAX_WAITING_REQUESTS=${GATEWAY_IMAGE_CONCURRENCY_MAX_WAITING_REQUESTS:-100}
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
redis:
|
||
condition: service_healthy
|
||
windsurf-ls:
|
||
condition: service_healthy
|
||
required: false
|
||
networks:
|
||
- sub2api-network
|
||
healthcheck:
|
||
test: ["CMD", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://localhost:8080/ready"]
|
||
interval: 30s
|
||
timeout: 10s
|
||
retries: 3
|
||
start_period: 30s
|
||
|
||
# ===========================================================================
|
||
# PostgreSQL Database
|
||
# ===========================================================================
|
||
postgres:
|
||
image: postgres:18-alpine
|
||
container_name: sub2api-postgres
|
||
restart: unless-stopped
|
||
ulimits:
|
||
nofile:
|
||
soft: 100000
|
||
hard: 100000
|
||
volumes:
|
||
- postgres_data:/var/lib/postgresql/data
|
||
environment:
|
||
# postgres:18-alpine 默认 PGDATA 在镜像内部匿名卷,必须显式指定才能持久化到命名卷
|
||
# 与旧版 compose 保持一致: PGDATA=/var/lib/postgresql/data,迁移无感
|
||
- PGDATA=/var/lib/postgresql/data
|
||
- POSTGRES_USER=${POSTGRES_USER:-sub2api}
|
||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
|
||
- POSTGRES_DB=${POSTGRES_DB:-sub2api}
|
||
- TZ=${TZ:-Asia/Shanghai}
|
||
# 默认不对外暴露端口。如需从宿主机调试,取消注释:
|
||
# ports:
|
||
# - "127.0.0.1:5433:5432"
|
||
networks:
|
||
- sub2api-network
|
||
healthcheck:
|
||
test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sub2api} -d ${POSTGRES_DB:-sub2api}" ]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
start_period: 15s
|
||
|
||
# ===========================================================================
|
||
# Redis Cache
|
||
# ===========================================================================
|
||
redis:
|
||
image: redis:8-alpine
|
||
container_name: sub2api-redis
|
||
restart: unless-stopped
|
||
ulimits:
|
||
nofile:
|
||
soft: 100000
|
||
hard: 100000
|
||
volumes:
|
||
- redis_data:/data
|
||
command: >
|
||
sh -c 'if [ -n "$$REDIS_PASSWORD" ]; then
|
||
exec redis-server --requirepass "$$REDIS_PASSWORD" --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru;
|
||
else
|
||
exec redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru;
|
||
fi'
|
||
environment:
|
||
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
||
# 默认不对外暴露端口。如需调试取消注释:
|
||
# ports:
|
||
# - "127.0.0.1:6380:6379"
|
||
networks:
|
||
- sub2api-network
|
||
healthcheck:
|
||
test: [ "CMD-SHELL", "if [ -n \"$$REDIS_PASSWORD\" ]; then redis-cli -a \"$$REDIS_PASSWORD\" ping; else redis-cli ping; fi" ]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
start_period: 10s
|
||
|
||
# ===========================================================================
|
||
# Windsurf Language Server (可选)
|
||
# ---------------------------------------------------------------------------
|
||
# 启用方式: docker compose --profile windsurf up -d
|
||
#
|
||
# 架构说明:
|
||
# - LS 本体只能绑 127.0.0.1:LS_INTERNAL_PORT(CSRF 仅允许 loopback peer)。
|
||
# - 容器内 socat 把 0.0.0.0:42099 → 127.0.0.1:42099,
|
||
# 使 compose 同网络内的服务可直接通过 `windsurf-ls:42099` 访问。
|
||
#
|
||
# 架构限制: 官方 LS 二进制仅提供 linux/amd64 & linux/arm64;非匹配平台通过 QEMU 模拟。
|
||
# 资源建议: CPU 1-2 cores, Memory 512MB - 1GB
|
||
# ===========================================================================
|
||
windsurf-ls:
|
||
image: docker.io/zfc931912343/sub2api-windsurf-ls:latest
|
||
container_name: sub2api-windsurf-ls
|
||
restart: unless-stopped
|
||
profiles: [ "windsurf" ]
|
||
volumes:
|
||
- windsurf_ls_data:/data
|
||
environment:
|
||
# Dockerfile.ls 的 ENTRYPOINT 消费这些键,不要改名
|
||
# LS_PORT : 容器对外暴露端口(socat 监听)
|
||
# LS_INTERNAL_PORT: LS 本体监听端口(socat 转发目的端口);两者必须不同
|
||
- LS_PORT=42099
|
||
- LS_INTERNAL_PORT=42098
|
||
- LS_CSRF_TOKEN=${WINDSURF_DOCKER_CSRF_TOKEN:?WINDSURF_DOCKER_CSRF_TOKEN is required when windsurf profile is enabled}
|
||
- LS_API_SERVER_URL=${LS_API_SERVER_URL:-https://server.self-serve.windsurf.com}
|
||
- HTTPS_PROXY=${LS_HTTPS_PROXY:-}
|
||
- HTTP_PROXY=${LS_HTTP_PROXY:-}
|
||
- TZ=${TZ:-Asia/Shanghai}
|
||
# 默认不对外暴露端口,仅通过内部网络通信
|
||
# 如需从宿主机调试,取消注释:
|
||
# ports:
|
||
# - "127.0.0.1:42099:42099"
|
||
networks:
|
||
- sub2api-network
|
||
healthcheck:
|
||
test: [ "CMD", "nc", "-z", "127.0.0.1", "42099" ]
|
||
interval: 10s
|
||
timeout: 3s
|
||
retries: 5
|
||
start_period: 20s
|
||
|
||
networks:
|
||
sub2api-network:
|
||
name: sub2api-network
|
||
driver: bridge
|
||
|
||
volumes:
|
||
sub2api_data:
|
||
name: sub2api_data
|
||
postgres_data:
|
||
name: sub2api_postgres_data
|
||
redis_data:
|
||
name: sub2api_redis_data
|
||
windsurf_ls_data:
|
||
name: sub2api_windsurf_ls_data
|