Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s

This commit is contained in:
李建琦
2026-08-21 18:30:13 +08:00
commit 6d655c9903
3584 changed files with 1270640 additions and 0 deletions
+458
View File
@@ -0,0 +1,458 @@
# =============================================================================
# Sub2API Container Environment Configuration
# =============================================================================
# Copy this file to .env and modify as needed:
# cp .env.example .env
# chmod 600 .env
# nano .env
#
# Then start with Docker Compose or Apple container:
# docker compose up -d
# ./apple-container.sh up
# =============================================================================
# -----------------------------------------------------------------------------
# Server Configuration
# -----------------------------------------------------------------------------
# IPv4 bind address for host port mapping
BIND_HOST=0.0.0.0
# Server port exposed on the host (Apple container requires 1025-65535)
SERVER_PORT=8080
# Server mode: release or debug
SERVER_MODE=release
# Optional token used only for GitHub Release API update checks. Release asset
# downloads remain anonymous. GITHUB_TOKEN and GH_TOKEN are not used.
UPDATE_GITHUB_TOKEN=
# Return Server-Timing for authenticated requests made by the Admin web UI
ENABLE_SERVER_TIMING=false
# Apple container image overrides (ignored by Docker Compose). Pin release tags
# or digests for repeatable operator-managed deployments.
APPLE_CONTAINER_SUB2API_IMAGE=weishaw/sub2api:latest
APPLE_CONTAINER_POSTGRES_IMAGE=postgres:18-alpine
APPLE_CONTAINER_REDIS_IMAGE=redis:8-alpine
# -----------------------------------------------------------------------------
# Logging Configuration
# 日志配置
# -----------------------------------------------------------------------------
# 日志级别:debug/info/warn/error
LOG_LEVEL=info
# 日志格式:json/console
LOG_FORMAT=json
# 每条日志附带的 service 字段
LOG_SERVICE_NAME=sub2api
# 每条日志附带的 env 字段
LOG_ENV=production
# 是否输出调用方位置信息
LOG_CALLER=true
# 堆栈输出阈值:none/error/fatal
LOG_STACKTRACE_LEVEL=error
# 输出开关(建议容器内保持双输出)
# 是否输出到 stdout/stderr
LOG_OUTPUT_TO_STDOUT=true
# 是否输出到文件
LOG_OUTPUT_TO_FILE=true
# 日志文件路径(留空自动推导):
# - 设置 DATA_DIR${DATA_DIR}/logs/sub2api.log
# - 未设置 DATA_DIR/app/data/logs/sub2api.log
LOG_OUTPUT_FILE_PATH=
# 滚动配置
# 单文件最大体积(MB
LOG_ROTATION_MAX_SIZE_MB=100
# 保留历史文件数量(0 表示不限制)
LOG_ROTATION_MAX_BACKUPS=10
# 历史日志保留天数(0 表示不限制)
LOG_ROTATION_MAX_AGE_DAYS=7
# 是否压缩历史日志
LOG_ROTATION_COMPRESS=true
# 滚动文件时间戳是否使用本地时间
LOG_ROTATION_LOCAL_TIME=true
# 采样配置(高频重复日志降噪)
LOG_SAMPLING_ENABLED=false
# 每秒前 N 条日志不采样
LOG_SAMPLING_INITIAL=100
# 之后每 N 条保留 1 条
LOG_SAMPLING_THEREAFTER=100
# Global max request body size in bytes (default: 256MB)
# 全局最大请求体大小(字节,默认 256MB)
# Applies to all requests, especially important for h2c first request memory protection
# 适用于所有请求,对 h2c 第一请求的内存保护尤为重要
SERVER_MAX_REQUEST_BODY_SIZE=268435456
# Gateway max request body size in bytes (default: 256MB)
# 网关请求体最大字节数(默认 256MB)
GATEWAY_MAX_BODY_SIZE=268435456
# Enable HTTP/2 Cleartext (h2c) for client connections
# 启用 HTTP/2 Cleartext (h2c) 客户端连接
SERVER_H2C_ENABLED=true
# H2C max concurrent streams (default: 50)
# H2C 最大并发流数量(默认 50)
SERVER_H2C_MAX_CONCURRENT_STREAMS=50
# H2C idle timeout in seconds (default: 75)
# H2C 空闲超时时间(秒,默认 75)
SERVER_H2C_IDLE_TIMEOUT=75
# H2C max read frame size in bytes (default: 1048576 = 1MB)
# H2C 最大帧大小(字节,默认 1048576 = 1MB
SERVER_H2C_MAX_READ_FRAME_SIZE=1048576
# H2C max upload buffer per connection in bytes (default: 2097152 = 2MB)
# H2C 每个连接的最大上传缓冲区(字节,默认 2097152 = 2MB
SERVER_H2C_MAX_UPLOAD_BUFFER_PER_CONNECTION=2097152
# H2C max upload buffer per stream in bytes (default: 524288 = 512KB)
# H2C 每个流的最大上传缓冲区(字节,默认 524288 = 512KB
SERVER_H2C_MAX_UPLOAD_BUFFER_PER_STREAM=524288
# 运行模式: standard (默认) 或 simple (内部自用)
# standard: 完整 SaaS 功能,包含计费/余额校验;simple: 隐藏 SaaS 功能并跳过计费/余额校验
RUN_MODE=standard
# Timezone
TZ=Asia/Shanghai
# Optional mobile Alipay flow. Unset uses the value saved in Admin Settings.
# Enable only for official Alipay instances with face-to-face payment enabled.
# ALIPAY_MOBILE_PRECREATE_DEEP_LINK=true
# -----------------------------------------------------------------------------
# PostgreSQL Configuration (REQUIRED)
# -----------------------------------------------------------------------------
POSTGRES_USER=sub2api
POSTGRES_PASSWORD=change_this_secure_password
POSTGRES_DB=sub2api
# PostgreSQL 监听端口(同时用于 PG 服务端和应用连接,默认 5432)
DATABASE_PORT=5432
# -----------------------------------------------------------------------------
# PostgreSQL 服务端参数(可选)
# -----------------------------------------------------------------------------
# POSTGRES_MAX_CONNECTIONSPostgreSQL 服务端允许的最大连接数。
# 必须 >=(所有 Sub2API 实例的 DATABASE_MAX_OPEN_CONNS 之和)+ 预留余量(例如 20%)。
POSTGRES_MAX_CONNECTIONS=1024
# POSTGRES_SHARED_BUFFERSPostgreSQL 用于缓存数据页的共享内存。
# 常见建议:物理内存的 10%~25%(容器内存受限时请按实际限制调整)。
# 8GB 内存容器参考:1GB。
POSTGRES_SHARED_BUFFERS=1GB
# POSTGRES_EFFECTIVE_CACHE_SIZE:查询规划器“假设可用的 OS 缓存大小”(不等于实际分配)。
# 常见建议:物理内存的 50%~75%。
# 8GB 内存容器参考:6GB。
POSTGRES_EFFECTIVE_CACHE_SIZE=4GB
# POSTGRES_MAINTENANCE_WORK_MEM:维护操作内存(VACUUM/CREATE INDEX 等)。
# 值越大维护越快,但会占用更多内存。
# 8GB 内存容器参考:128MB。
POSTGRES_MAINTENANCE_WORK_MEM=128MB
# -----------------------------------------------------------------------------
# PostgreSQL 连接池参数(可选,默认与程序内置一致)
# -----------------------------------------------------------------------------
# 说明:
# - 这些参数控制 Sub2API 进程到 PostgreSQL 的连接池大小(不是 PostgreSQL 自身的 max_connections)。
# - 多实例/多副本部署时,总连接上限约等于:实例数 * DATABASE_MAX_OPEN_CONNS。
# - 连接池过大可能导致:数据库连接耗尽、内存占用上升、上下文切换增多,反而变慢。
# - 建议结合 PostgreSQL 的 max_connections 与机器规格逐步调优:
# 通常把应用总连接上限控制在 max_connections 的 50%~80% 更稳妥。
#
# DATABASE_MAX_OPEN_CONNS:最大打开连接数(活跃+空闲),达到后新请求会等待可用连接。
# 典型范围:50~500(取决于 DB 规格、实例数、SQL 复杂度)。
DATABASE_MAX_OPEN_CONNS=256
# DATABASE_MAX_IDLE_CONNS:最大空闲连接数(热连接),建议 <= MAX_OPEN。
# 太小会频繁建连增加延迟;太大会长期占用数据库资源。
DATABASE_MAX_IDLE_CONNS=128
# DATABASE_CONN_MAX_LIFETIME_MINUTES:单个连接最大存活时间(单位:分钟)。
# 用于避免连接长期不重建导致的中间件/LB/NAT 异常或服务端重启后的“僵尸连接”。
# 设置为 0 表示不限制(一般不建议生产环境)。
DATABASE_CONN_MAX_LIFETIME_MINUTES=30
# DATABASE_CONN_MAX_IDLE_TIME_MINUTES:空闲连接最大存活时间(单位:分钟)。
# 超过该时间的空闲连接会被回收,防止长时间闲置占用连接数。
# 设置为 0 表示不限制(一般不建议生产环境)。
DATABASE_CONN_MAX_IDLE_TIME_MINUTES=5
# -----------------------------------------------------------------------------
# Redis Configuration
# -----------------------------------------------------------------------------
# Redis 监听端口(同时用于应用连接和 Redis 服务端,默认 6379)
REDIS_PORT=6379
# Redis ACL username; leave empty for the default user
REDIS_USERNAME=
# Leave empty for no password (default for local development)
REDIS_PASSWORD=
REDIS_DB=0
# Redis 服务端最大客户端连接数(可选)
REDIS_MAXCLIENTS=50000
# Redis 连接池大小(默认 1024)
REDIS_POOL_SIZE=4096
# Redis 最小空闲连接数(默认 10)
REDIS_MIN_IDLE_CONNS=256
REDIS_ENABLE_TLS=false
# -----------------------------------------------------------------------------
# Admin Account
# -----------------------------------------------------------------------------
# Email for the admin account
ADMIN_EMAIL=admin@sub2api.local
# Password for admin account
# Leave empty to auto-generate (will be shown in logs on first run)
ADMIN_PASSWORD=
# -----------------------------------------------------------------------------
# JWT Configuration
# -----------------------------------------------------------------------------
# IMPORTANT: Set a fixed JWT_SECRET to prevent login sessions from being
# invalidated after container restarts. If left empty, a random secret will
# be generated on each startup, causing all users to be logged out.
# Generate a secure secret: openssl rand -hex 32
JWT_SECRET=
JWT_EXPIRE_HOUR=24
# Access Token 有效期(分钟)
# 优先级说明:
# - >0: 按分钟生效(优先于 JWT_EXPIRE_HOUR
# - =0: 回退使用 JWT_EXPIRE_HOUR
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=0
# -----------------------------------------------------------------------------
# Setup Configuration
# -----------------------------------------------------------------------------
# Database migration timeout during initial setup, in seconds.
# Leave 0 to use the built-in default of 60 seconds.
SETUP_MIGRATION_TIMEOUT_SECONDS=0
# -----------------------------------------------------------------------------
# TOTP (2FA) Configuration
# TOTP(双因素认证)配置
# -----------------------------------------------------------------------------
# IMPORTANT: Set a fixed encryption key for TOTP secrets. If left empty, a
# random key will be generated on each startup, causing all existing TOTP
# configurations to become invalid (users won't be able to login with 2FA).
# Generate a secure key: openssl rand -hex 32
# 重要:设置固定的 TOTP 加密密钥。如果留空,每次启动将生成随机密钥,
# 导致现有的 TOTP 配置失效(用户无法使用双因素认证登录)。
TOTP_ENCRYPTION_KEY=
# -----------------------------------------------------------------------------
# Configuration File (Optional)
# -----------------------------------------------------------------------------
# Path to custom config file (relative to docker-compose.yml directory)
# Copy config.example.yaml to config.yaml and modify as needed
# Leave unset to use default ./config.yaml
#CONFIG_FILE=./config.yaml
# -----------------------------------------------------------------------------
# Built-in OAuth Client Secrets (Optional)
# -----------------------------------------------------------------------------
# SECURITY NOTE:
# - 本项目不会在代码仓库中内置第三方 OAuth client_secret。
# - 如需使用“内置客户端”(而不是自建 OAuth Client),请在运行环境通过 env 注入。
#
# Gemini CLI built-in OAuth client_secret(用于 Gemini code_assist/google_one 内置登录流)
# GEMINI_CLI_OAUTH_CLIENT_SECRET=
#
# Antigravity OAuth client_secret(用于 Antigravity OAuth 登录流)
# ANTIGRAVITY_OAUTH_CLIENT_SECRET=
#
# Antigravity User-Agent 版本号(后台设置 antigravity_user_agent_version 优先;留空使用内置默认 1.23.2)
# ANTIGRAVITY_USER_AGENT_VERSION=
# -----------------------------------------------------------------------------
# Rate Limiting (Optional)
# 速率限制(可选)
# -----------------------------------------------------------------------------
# Cooldown time (in minutes) when upstream returns 529 (overloaded)
# 上游返回 529(过载)时的冷却时间(分钟)
RATE_LIMIT_OVERLOAD_COOLDOWN_MINUTES=10
# -----------------------------------------------------------------------------
# Gateway Scheduling (Optional)
# 调度缓存与受控回源配置(缓存就绪且命中时不读 DB)
# -----------------------------------------------------------------------------
# Force Codex CLI mode: treat all /openai/v1/responses requests as Codex CLI.
# 强制按 Codex CLI 处理 /openai/v1/responses 请求(用于网关未透传/改写 User-Agent 的兜底)。
#
# 注意:开启后会影响所有客户端的行为(不仅限于 VS Code / Codex CLI),请谨慎开启。
#
# 默认:false
GATEWAY_FORCE_CODEX_CLI=false
# OpenAI /responses/compact 上游模型(默认 gpt-5.4)。
# 当 compact 端点暂未支持更新模型时,可通过这里降级规避失败。
GATEWAY_OPENAI_COMPACT_MODEL=gpt-5.4
# OpenAI/Codex 等待上游响应头超时(秒);0 表示不使用本地响应头超时截断。
GATEWAY_OPENAI_RESPONSE_HEADER_TIMEOUT=0
# OpenAI HTTP 上游默认启用 HTTP/2;如需紧急回滚可设为 false。
GATEWAY_OPENAI_HTTP2_ENABLED=true
GATEWAY_OPENAI_HTTP2_ALLOW_PROXY_FALLBACK_TO_HTTP1=true
GATEWAY_OPENAI_HTTP2_FALLBACK_ERROR_THRESHOLD=2
GATEWAY_OPENAI_HTTP2_FALLBACK_WINDOW_SECONDS=60
GATEWAY_OPENAI_HTTP2_FALLBACK_TTL_SECONDS=600
GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_FAILURE_THRESHOLD=2
GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_WINDOW_SECONDS=60
GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_TTL_SECONDS=600
# 上游连接池:每主机最大连接数(默认 1024;流式/HTTP1.1 场景可调大,如 2400/4096
GATEWAY_MAX_CONNS_PER_HOST=2048
# 上游连接池:最大空闲连接总数(默认 2560;账号/代理隔离 + 高并发场景可调大)
GATEWAY_MAX_IDLE_CONNS=8192
# 上游连接池:每主机最大空闲连接(默认 120)
GATEWAY_MAX_IDLE_CONNS_PER_HOST=4096
# 粘性会话最大排队长度
GATEWAY_SCHEDULING_STICKY_SESSION_MAX_WAITING=3
# 粘性会话等待超时(时间段,例如 45s)
GATEWAY_SCHEDULING_STICKY_SESSION_WAIT_TIMEOUT=120s
# 兜底排队等待超时(时间段,例如 30s)
GATEWAY_SCHEDULING_FALLBACK_WAIT_TIMEOUT=30s
# 兜底最大排队长度
GATEWAY_SCHEDULING_FALLBACK_MAX_WAITING=100
# 启用调度批量负载计算
GATEWAY_SCHEDULING_LOAD_BATCH_ENABLED=true
# 并发槽位清理周期(时间段,例如 30s)
GATEWAY_SCHEDULING_SLOT_CLEANUP_INTERVAL=30s
# 是否允许受控回源到 DB(默认 true,保持现有行为)
GATEWAY_SCHEDULING_DB_FALLBACK_ENABLED=true
# 受控回源超时(秒),0 表示不额外收紧超时
GATEWAY_SCHEDULING_DB_FALLBACK_TIMEOUT_SECONDS=0
# 受控回源限流(实例级 QPS),0 表示不限制
GATEWAY_SCHEDULING_DB_FALLBACK_MAX_QPS=0
# outbox 轮询周期(秒)
GATEWAY_SCHEDULING_OUTBOX_POLL_INTERVAL_SECONDS=1
# outbox 滞后告警阈值(秒)
GATEWAY_SCHEDULING_OUTBOX_LAG_WARN_SECONDS=5
# outbox 触发强制重建阈值(秒)
GATEWAY_SCHEDULING_OUTBOX_LAG_REBUILD_SECONDS=10
# outbox 连续滞后触发次数
GATEWAY_SCHEDULING_OUTBOX_LAG_REBUILD_FAILURES=3
# outbox 积压触发重建阈值(行数)
GATEWAY_SCHEDULING_OUTBOX_BACKLOG_REBUILD_ROWS=10000
# 全量重建周期(秒)
GATEWAY_SCHEDULING_FULL_REBUILD_INTERVAL_SECONDS=300
# -----------------------------------------------------------------------------
# Image Generation Keepalive & Concurrency (Optional)
# 图片生成保活与并发隔离配置(可选)
# -----------------------------------------------------------------------------
# 图片流式上游数据间隔超时(秒)。0 表示禁用;非 0 时必须为 60-1800。
GATEWAY_IMAGE_STREAM_DATA_INTERVAL_TIMEOUT=900
# 图片流式 keepalive 间隔(秒)。0 表示禁用;非 0 时必须为 5-60。
GATEWAY_IMAGE_STREAM_KEEPALIVE_INTERVAL=10
# 图片非流式 JSON keepalive 间隔(秒)。默认 0 禁用;首个心跳后 HTTP 状态会固化为 200。
GATEWAY_IMAGE_NONSTREAM_KEEPALIVE_INTERVAL=0
# 是否启用进程级图片生成并发限制。默认 false,保持历史行为。
GATEWAY_IMAGE_CONCURRENCY_ENABLED=false
# 当前进程允许同时处理的图片生成请求数。0 表示不限制。
GATEWAY_IMAGE_CONCURRENCY_MAX_CONCURRENT_REQUESTS=0
# 图片并发超限策略:reject 直接返回 429;wait 等待空闲槽位。
GATEWAY_IMAGE_CONCURRENCY_OVERFLOW_MODE=reject
# wait 模式下等待空闲图片槽位的最长时间(秒)。
GATEWAY_IMAGE_CONCURRENCY_WAIT_TIMEOUT_SECONDS=30
# wait 模式下当前进程允许排队等待的最大图片请求数。0 表示不允许等待队列。
GATEWAY_IMAGE_CONCURRENCY_MAX_WAITING_REQUESTS=100
# -----------------------------------------------------------------------------
# Dashboard Aggregation (Optional)
# -----------------------------------------------------------------------------
# Enable aggregation job
# 启用仪表盘预聚合
DASHBOARD_AGGREGATION_ENABLED=true
# Refresh interval (seconds)
# 刷新间隔(秒)
DASHBOARD_AGGREGATION_INTERVAL_SECONDS=60
# Lookback window (seconds)
# 回看窗口(秒)
DASHBOARD_AGGREGATION_LOOKBACK_SECONDS=120
# Allow manual backfill
# 允许手动回填
DASHBOARD_AGGREGATION_BACKFILL_ENABLED=false
# Backfill max range (days)
# 回填最大跨度(天)
DASHBOARD_AGGREGATION_BACKFILL_MAX_DAYS=31
# Recompute recent N days on startup
# 启动时重算最近 N 天
DASHBOARD_AGGREGATION_RECOMPUTE_DAYS=2
# Retention windows (days)
# 保留窗口(天)
DASHBOARD_AGGREGATION_RETENTION_USAGE_LOGS_DAYS=90
DASHBOARD_AGGREGATION_RETENTION_HOURLY_DAYS=180
DASHBOARD_AGGREGATION_RETENTION_DAILY_DAYS=730
# -----------------------------------------------------------------------------
# Security Configuration
# -----------------------------------------------------------------------------
# URL Allowlist Configuration
# 启用 URL 白名单验证(false 则跳过白名单检查,仅做基本格式校验)
SECURITY_URL_ALLOWLIST_ENABLED=false
# 关闭白名单时,是否允许 http:// URL(默认 true,设为 false 则只允许 https://
# ⚠️ 警告:允许 HTTP 存在安全风险(明文传输),生产环境建议设为 false
# Allow insecure HTTP URLs when allowlist is disabled (default: true; set to false to require https)
# ⚠️ WARNING: Allowing HTTP has security risks (plaintext transmission)
# Recommended to set false in production
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=true
# 是否允许本地/私有 IP 地址用于上游/定价/CRS(仅在可信网络中使用)
# Allow localhost/private IPs for upstream/pricing/CRS (use only in trusted networks)
SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=true
# -----------------------------------------------------------------------------
# Gemini OAuth (OPTIONAL, required only for Gemini OAuth accounts)
# -----------------------------------------------------------------------------
# Sub2API supports TWO Gemini OAuth modes:
#
# 1. Code Assist OAuth (需要 GCP project_id)
# - Uses: cloudcode-pa.googleapis.com (Code Assist API)
# - Auto scopes: cloud-platform + userinfo.email + userinfo.profile
# - OAuth Client: Can use built-in Gemini CLI client (留空即可)
# - Requires: Google Cloud Platform project with Code Assist enabled
#
# 2. AI Studio OAuth (不需要 project_id)
# - Uses: generativelanguage.googleapis.com (AI Studio API)
# - Default scopes: generative-language
# - OAuth Client: Requires your own OAuth 2.0 Client (内置 Gemini CLI client 不能申请 generative-language scope)
# - Requires: Create OAuth 2.0 Client in GCP Console + OAuth consent screen
# - Setup Guide: https://ai.google.dev/gemini-api/docs/oauth
# - ⚠️ IMPORTANT: OAuth Client 必须发布为正式版本 (Production)
# Testing 模式限制: 只能添加 100 个测试用户, refresh token 7 天后过期
# 发布步骤: GCP Console → OAuth consent screen → PUBLISH APP
#
# Configuration:
# Leave empty to use the built-in Gemini CLI OAuth client (Code Assist OAuth only).
# To enable AI Studio OAuth, set your own OAuth client ID/secret here.
GEMINI_OAUTH_CLIENT_ID=
GEMINI_OAUTH_CLIENT_SECRET=
# Optional; leave empty to auto-select scopes based on oauth_type
GEMINI_OAUTH_SCOPES=
# -----------------------------------------------------------------------------
# Gemini Quota Policy (OPTIONAL, local simulation)
# -----------------------------------------------------------------------------
# JSON overrides for local quota simulation (Code Assist only).
# Example:
# GEMINI_QUOTA_POLICY={"tiers":{"LEGACY":{"pro_rpd":50,"flash_rpd":1500,"cooldown_minutes":30},"PRO":{"pro_rpd":1500,"flash_rpd":4000,"cooldown_minutes":5},"ULTRA":{"pro_rpd":2000,"flash_rpd":0,"cooldown_minutes":5}}}
GEMINI_QUOTA_POLICY=
# -----------------------------------------------------------------------------
# Ops Monitoring Configuration (运维监控配置)
# -----------------------------------------------------------------------------
# Enable ops monitoring features (background jobs and APIs)
# 是否启用运维监控功能(后台任务和接口)
# Set to false to hide ops menu in sidebar and disable all ops features
# 设置为 false 可在左侧栏隐藏运维监控菜单并禁用所有运维监控功能
OPS_ENABLED=true
# -----------------------------------------------------------------------------
# Update Configuration (在线更新配置)
# -----------------------------------------------------------------------------
# Proxy URL for accessing GitHub (used for online updates and pricing data)
# 用于访问 GitHub 的代理地址(用于在线更新和定价数据获取)
# Supports: http, https, socks5, socks5h
# Examples:
# HTTP proxy: http://127.0.0.1:7890
# SOCKS5 proxy: socks5://127.0.0.1:1080
# With authentication: http://user:pass@proxy.example.com:8080
# Leave empty for direct connection (recommended for overseas servers)
# 留空表示直连(适用于海外服务器)
UPDATE_PROXY_URL=
+19
View File
@@ -0,0 +1,19 @@
# =============================================================================
# Sub2API Deploy Directory - Git Ignore
# =============================================================================
# Data directories (generated at runtime when using docker-compose.local.yml)
data/
postgres_data/
redis_data/
# Environment configuration (contains sensitive information)
.env
# Backup files
*.backup
*.bak
# Temporary files
*.tmp
*.log
+221
View File
@@ -0,0 +1,221 @@
# Apple container Deployment
Sub2API can run as a native three-service stack with Apple's `container` CLI. This workflow runs the published Sub2API, PostgreSQL, and Redis OCI images without Docker Desktop or a Docker-compatible daemon.
## Support Level
Apple `container` support is intended for local development and operator-managed deployments on a Mac. Docker Compose remains the recommended production deployment path.
Apple `container` 1.1 does not provide restart policies, automatic startup, workload health scheduling, a Docker API socket, or full Compose orchestration. `apple-container.sh` supplies ordered startup and readiness checks when you invoke it, but it is not a continuously running supervisor.
## Requirements
- A Mac with Apple silicon
- macOS 26 or newer
- Apple `container` 1.1.0 or newer
- `openssl` for generating initial secrets
- Local Network access for `container-runtime-linux` when macOS prompts during the first published-container startup
Install Apple `container` from its [official releases](https://github.com/apple/container/releases), then verify it:
```bash
container --version
```
## Quick Start
```bash
git clone https://github.com/Wei-Shaw/sub2api.git
cd sub2api/deploy
# Creates .env with random PostgreSQL, JWT, and TOTP secrets.
./apple-container.sh init
# Review optional settings before startup.
nano .env
# Creates volumes/network/containers, waits for dependencies, and starts Sub2API.
./apple-container.sh up
# Verifies PostgreSQL, Redis, and the application endpoint.
./apple-container.sh status
```
Open `http://localhost:8080`. If `ADMIN_PASSWORD` is empty, retrieve the generated password with:
```bash
./apple-container.sh logs app
```
The env file uses literal `KEY=value` syntax. Do not use Compose expressions such as `${VALUE:-default}`, and do not quote values unless the quote characters are part of the intended value. `BIND_HOST` must be an IPv4 address, and `SERVER_PORT` must be between 1025 and 65535.
## Commands
```bash
# Start dependencies and recreate the lightweight app container with current IPs.
./apple-container.sh up
# Also recreate PostgreSQL and Redis containers, preserving their volumes.
./apple-container.sh up --recreate
# Stop containers while preserving all resources and data.
./apple-container.sh down
# Restart PostgreSQL, Redis, and Sub2API in dependency order.
./apple-container.sh restart
# Show resource state and run live health probes.
./apple-container.sh status
# Follow one service's logs.
./apple-container.sh logs app -f
./apple-container.sh logs postgres -f
./apple-container.sh logs redis -f
# Pull all configured images for linux/arm64, then recreate containers.
./apple-container.sh pull
./apple-container.sh up --recreate
# Delete containers and the network, preserving named volumes.
./apple-container.sh destroy --yes
# Permanently delete the stack and all application/database/cache data.
./apple-container.sh destroy --volumes --yes
```
`destroy --volumes` does not remove `.env`, backup files, or pulled images. Delete credentials and backups separately when decommissioning a deployment. Use `container image delete <image>` only after confirming no other Apple containers use that image.
After a host reboot or `container system stop`, run `./apple-container.sh up` again. Apple `container` does not automatically restart persisted containers.
## Configuration
The script uses `deploy/.env`, the same source file used by Docker Compose. Export `SUB2API_ENV_FILE` to use another file for every command in the current shell:
```bash
export SUB2API_ENV_FILE=/absolute/path/to/sub2api.env
./apple-container.sh init
./apple-container.sh up
```
Apple-specific image overrides are available:
```dotenv
APPLE_CONTAINER_SUB2API_IMAGE=weishaw/sub2api:latest
APPLE_CONTAINER_POSTGRES_IMAGE=postgres:18-alpine
APPLE_CONTAINER_REDIS_IMAGE=redis:8-alpine
```
The normal `up` command recreates the application container, so application environment changes are applied immediately. Use `up --recreate` when changing PostgreSQL or Redis container images or Redis runtime configuration. Persistent data remains in named volumes.
`POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` are applied only when PostgreSQL initializes an empty data volume. Changing them in `.env` and recreating the container does not change an existing database. Rotate a password with `ALTER ROLE`, and plan explicit migrations for user or database changes. To intentionally initialize a new empty database, first back up the old one and use `destroy --volumes`.
Apple-specific handling of shared settings:
| Setting | Apple workflow behavior |
|---|---|
| Application and gateway variables | Passed to Sub2API from `.env` |
| `BIND_HOST`, `SERVER_PORT` | Used for the macOS published port |
| `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | PostgreSQL first initialization only |
| `REDIS_PASSWORD` | Applied to Redis and Sub2API |
| `DATABASE_PORT`, `REDIS_PORT` | Internal ports are fixed to 5432 and 6379 |
| `POSTGRES_MAX_*`, `REDIS_MAXCLIENTS` | Not currently applied to the database/cache server |
## Managed Resources
The script creates only resources carrying the `org.sub2api.stack=apple-container` label:
| Type | Names |
|---|---|
| Containers | `sub2api-apple`, `sub2api-apple-postgres`, `sub2api-apple-redis` |
| Network | `sub2api-apple` |
| Volumes | `sub2api-apple-data`, `sub2api-apple-postgres-data`, `sub2api-apple-redis-data` |
The PostgreSQL volume is mounted at `/var/lib/postgresql`, retaining PostgreSQL 18's default child data directory. Sub2API and Redis also store data in child directories below their Apple volume mount points. This is required because Apple named volumes do not have Docker's copy-up and mount-point ownership behavior.
## Networking
Apple `container` 1.1 does not provide Compose-style network-scoped service aliases. After PostgreSQL and Redis start, the script reads their current private-network IPv4 addresses from `container inspect`, injects those addresses into a newly created application container, and then starts Sub2API. The script does not modify `~/.config/container/config.toml` or the macOS host resolver.
All three services attach only to the private `sub2api-apple` network. Only the application publishes a host port; database and Redis ports remain unpublished.
The application container is intentionally recreated by every `up` and `restart` operation because dependency VM addresses can change after they stop. Application data remains in `sub2api-apple-data`.
The script checks the published `/health` endpoint from macOS before reporting success. Approve the Local Network prompt on first startup. If the internal probe succeeds but the host-port probe fails with a connection reset, enable Local Network access for `container-runtime-linux`, run `container system stop` followed by `container system start`, and then run `up` again. Runtime upgrades may prompt for permission again.
## Backup and Upgrade
Pin image release tags or digests in `.env` before using this workflow for persistent data. Before an application or database image upgrade, create backups while the stack is healthy:
```bash
umask 077
mkdir -p backups
# Logical PostgreSQL backup.
container exec sub2api-apple sh -c \
'PGPASSWORD="$DATABASE_PASSWORD" pg_dump -h "$DATABASE_HOST" -U "$DATABASE_USER" "$DATABASE_DBNAME"' \
> backups/sub2api.sql
# Application configuration and local files.
container exec sub2api-apple sh -c 'tar -C "$DATA_DIR" -czf - .' \
> backups/sub2api-data.tar.gz
./apple-container.sh pull
./apple-container.sh up --recreate
./apple-container.sh status
```
Database migrations are forward-only. Keep the previous image reference and both backups until the upgraded stack has been validated; image rollback alone cannot reverse a migrated database. Test restore procedures before relying on this workflow for important data.
To restore these backups into an existing stack, first ensure the image versions are compatible with the backup, then stop writers and replace both data sets:
```bash
# Ensure empty/current resources exist, then stop the stack.
./apple-container.sh up
./apple-container.sh down
# Remove only the app container so a helper can mount its named volume.
container delete sub2api-apple
SUB2API_IMAGE=weishaw/sub2api:latest # Match APPLE_CONTAINER_SUB2API_IMAGE in .env.
container run --rm --name sub2api-apple-data-restore \
--entrypoint /bin/sh \
--volume sub2api-apple-data:/restore \
--volume "$PWD/backups:/backup:ro" \
"$SUB2API_IMAGE" \
-c 'rm -rf /restore/data && mkdir -p /restore/data && tar -xzf /backup/sub2api-data.tar.gz -C /restore/data'
# Restore the logical database while the application is absent.
container start sub2api-apple-postgres
until container exec sub2api-apple-postgres sh -c 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"'; do sleep 1; done
container copy backups/sub2api.sql sub2api-apple-postgres:/tmp/sub2api.sql
container exec sub2api-apple-postgres sh -c '
export PGPASSWORD="$POSTGRES_PASSWORD"
dropdb -h 127.0.0.1 -U "$POSTGRES_USER" --if-exists --force "$POSTGRES_DB"
createdb -h 127.0.0.1 -U "$POSTGRES_USER" "$POSTGRES_DB"
psql -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -f /tmp/sub2api.sql
rm /tmp/sub2api.sql
'
./apple-container.sh up
./apple-container.sh status
```
For disaster recovery after deleting the named volumes, run `up` once to create a fresh stack before following the restore sequence. Perform restore drills with non-production data first.
To upgrade the Apple runtime itself:
```bash
./apple-container.sh down
container system stop
# Install/update Apple container 1.1.0 or newer.
container system start
./apple-container.sh up
```
## Operational Limitations
- There is no `restart: unless-stopped` equivalent. Run `up` after reboot, or add your own launchd supervisor.
- Health probes run during `up`, `restart`, and `status`; Apple `container` does not continuously schedule them.
- Docker Compose, Testcontainers, Buildx, and tools requiring `/var/run/docker.sock` cannot use this runtime directly.
- Named volume backup and restore must be tested before using this workflow for important data.
- The script targets native `linux/arm64` images. The normal Sub2API release publishes an arm64 variant.
- Runtime environment values, including credentials, are retained in Apple container configuration and are visible to users who can inspect the local runtime.
+112
View File
@@ -0,0 +1,112 @@
{
servers {
max_header_size 64KB
timeouts {
read_header 10s
idle 2m
}
}
}
# 修改为你的域名
api.sub2api.com {
# This baseline assumes clients connect directly to Caddy. When Caddy is
# behind a CDN, configure explicit trusted proxy CIDRs and {client_ip} as
# documented in EDGE_SECURITY.md; {remote_host} would otherwise be the CDN.
# =========================================================================
# TLS 安全配置
# =========================================================================
tls {
# 仅使用 TLS 1.2 和 1.3
protocols tls1.2 tls1.3
# 优先使用的加密套件
ciphers TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
}
# =========================================================================
# 反向代理配置
# =========================================================================
reverse_proxy localhost:8080 {
# 健康检查
health_uri /health
health_interval 30s
health_timeout 10s
health_status 200
# 负载均衡策略(单节点可忽略,多节点时有用)
lb_policy round_robin
lb_try_duration 5s
lb_try_interval 250ms
# 仅从实际 TCP 对端生成转发头,避免透传客户端伪造值。
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Host {host}
# 连接池优化
transport http {
keepalive 120s
keepalive_idle_conns 256
read_buffer 16KB
write_buffer 16KB
compression off
}
# 故障转移
fail_duration 30s
max_fails 3
unhealthy_status 500 502 503 504
}
# =========================================================================
# 压缩配置
# =========================================================================
encode {
zstd
gzip 6
minimum_length 256
match {
# 不使用 text/*,否则 text/event-stream 会被压缩并缓冲到流结束
header Content-Type text/css*
header Content-Type text/csv*
header Content-Type text/html*
header Content-Type text/javascript*
header Content-Type text/markdown*
header Content-Type text/plain*
header Content-Type text/xml*
header Content-Type application/json*
header Content-Type application/javascript*
header Content-Type application/xml*
header Content-Type application/rss+xml*
header Content-Type image/svg+xml*
}
}
# =========================================================================
# 请求大小限制 (防止大文件攻击)
# =========================================================================
request_body {
max_size 256MB
}
# =========================================================================
# 日志配置
# =========================================================================
log {
output file /var/log/caddy/sub2api.log {
roll_size 50mb
roll_keep 10
roll_keep_for 720h
}
format json
level INFO
}
# =========================================================================
# 错误处理
# =========================================================================
handle_errors {
respond "{err.status_code} {err.status_text}"
}
}
+78
View File
@@ -0,0 +1,78 @@
# datamanagementd 部署说明(数据管理)
本文说明如何在宿主机部署 `datamanagementd`,并与主进程联动开启“数据管理”功能。
## 1. 关键约束
- 主进程固定探测路径:`/tmp/sub2api-datamanagement.sock`
- 仅当该 Unix Socket 可连通且 `Health` 成功时,后台“数据管理”才会启用
- `datamanagementd` 使用 SQLite 持久化元数据,不依赖主库
## 2. 宿主机构建与运行
```bash
cd /opt/sub2api-src/datamanagement
go build -o /opt/sub2api/datamanagementd ./cmd/datamanagementd
mkdir -p /var/lib/sub2api/datamanagement
chown -R sub2api:sub2api /var/lib/sub2api/datamanagement
```
手动启动示例:
```bash
/opt/sub2api/datamanagementd \
-socket-path /tmp/sub2api-datamanagement.sock \
-sqlite-path /var/lib/sub2api/datamanagement/datamanagementd.db \
-version 1.0.0
```
## 3. systemd 托管(推荐)
仓库已提供示例服务文件:`deploy/sub2api-datamanagementd.service`
```bash
sudo cp deploy/sub2api-datamanagementd.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now sub2api-datamanagementd
sudo systemctl status sub2api-datamanagementd
```
查看日志:
```bash
sudo journalctl -u sub2api-datamanagementd -f
```
也可以使用一键安装脚本(自动安装二进制 + 注册 systemd):
```bash
# 方式一:使用现成二进制
sudo ./deploy/install-datamanagementd.sh --binary /path/to/datamanagementd
# 方式二:从源码构建后安装
sudo ./deploy/install-datamanagementd.sh --source /path/to/sub2api
```
## 4. Docker 部署联动
`sub2api` 运行在 Docker 容器中,需要将宿主机 Socket 挂载到容器同路径:
```yaml
services:
sub2api:
volumes:
- /tmp/sub2api-datamanagement.sock:/tmp/sub2api-datamanagement.sock
```
建议在 `docker-compose.override.yml` 中维护该挂载,避免覆盖主 compose 文件。
## 5. 依赖检查
`datamanagementd` 执行备份时依赖以下工具:
- `pg_dump`
- `redis-cli`
- `docker`(仅 `source_mode=docker_exec` 时)
缺失依赖会导致对应任务失败,并在任务详情中体现错误信息。
+76
View File
@@ -0,0 +1,76 @@
# Sub2API Docker Image
Sub2API is an AI API Gateway Platform for distributing and managing AI product subscription API quotas.
## Quick Start
```bash
docker run -d \
--name sub2api \
-p 8080:8080 \
-e DATABASE_URL="postgres://user:pass@host:5432/sub2api" \
-e REDIS_URL="redis://host:6379" \
weishaw/sub2api:latest
```
## Docker Compose
```yaml
version: '3.8'
services:
sub2api:
image: weishaw/sub2api:latest
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/sub2api?sslmode=disable
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=sub2api
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
```
## Environment Variables
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `DATABASE_URL` | PostgreSQL connection string | Yes | - |
| `REDIS_URL` | Redis connection string | Yes | - |
| `PORT` | Server port | No | `8080` |
| `GIN_MODE` | Gin framework mode (`debug`/`release`) | No | `release` |
## Supported Architectures
- `linux/amd64`
- `linux/arm64`
## Tags
- `latest` - Latest stable release
- `x.y.z` - Specific version
- `x.y` - Latest patch of minor version
- `x` - Latest minor of major version
## Links
- [GitHub Repository](https://github.com/weishaw/sub2api)
- [Documentation](https://github.com/weishaw/sub2api#readme)
+126
View File
@@ -0,0 +1,126 @@
# =============================================================================
# Sub2API Multi-Stage Dockerfile
# =============================================================================
# Stage 1: Build frontend
# Stage 2: Build Go backend with embedded frontend
# Stage 3: Final minimal image
# =============================================================================
ARG NODE_IMAGE=node:24-alpine
ARG GOLANG_IMAGE=golang:1.26.6-alpine
ARG ALPINE_IMAGE=alpine:3.20
ARG GOPROXY=https://goproxy.cn,direct
ARG GOSUMDB=sum.golang.google.cn
# -----------------------------------------------------------------------------
# Stage 1: Frontend Builder
# -----------------------------------------------------------------------------
FROM ${NODE_IMAGE} AS frontend-builder
WORKDIR /app/frontend
ENV NODE_OPTIONS=--max-old-space-size=1536
# Install pnpm. Keep this pinned to the lockfile-compatible major version so
# Docker builds remain reproducible when pnpm changes config validation rules.
RUN corepack enable && corepack prepare pnpm@9.15.9 --activate
# Install dependencies first (better caching)
COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Copy frontend source and build.
# LegalDocumentView.vue (admin-compliance gate) build-time imports
# ../../../../docs/legal/*.md?raw, so docs/legal/ must sit beside frontend/
# in the image (WORKDIR /app/frontend -> resolves to /app/docs/legal/*.md).
# Copy only that subtree to keep the build dependency minimal.
COPY frontend/ ./
COPY docs/legal/ /app/docs/legal/
RUN pnpm run build
# -----------------------------------------------------------------------------
# Stage 2: Backend Builder
# -----------------------------------------------------------------------------
FROM ${GOLANG_IMAGE} AS backend-builder
# Build arguments for version info (set by CI)
ARG VERSION=
ARG COMMIT=docker
ARG DATE
ARG GOPROXY
ARG GOSUMDB
ENV GOPROXY=${GOPROXY}
ENV GOSUMDB=${GOSUMDB}
# Install build dependencies
RUN apk add --no-cache git ca-certificates tzdata
WORKDIR /app/backend
# Copy go mod files first (better caching)
COPY backend/go.mod backend/go.sum ./
RUN go mod download
# Copy backend source first
COPY backend/ ./
# Copy frontend dist from previous stage (must be after backend copy to avoid being overwritten)
COPY --from=frontend-builder /app/backend/internal/web/dist ./internal/web/dist
# Build the binary (BuildType=release for CI builds, embed frontend)
# Version precedence: build arg VERSION > exact git tag > cmd/server/VERSION
RUN VERSION_VALUE="${VERSION}" && \
if [ -z "${VERSION_VALUE}" ]; then VERSION_VALUE="$(./scripts/resolve-version.sh)"; fi && \
DATE_VALUE="${DATE:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}" && \
CGO_ENABLED=0 GOOS=linux go build \
-tags embed \
-ldflags="-s -w -X main.Version=${VERSION_VALUE} -X main.Commit=${COMMIT} -X main.Date=${DATE_VALUE} -X main.BuildType=release" \
-o /app/sub2api \
./cmd/server
# -----------------------------------------------------------------------------
# Stage 3: Final Runtime Image
# -----------------------------------------------------------------------------
FROM ${ALPINE_IMAGE}
# Labels
LABEL maintainer="Wei-Shaw <github.com/Wei-Shaw>"
LABEL description="Sub2API - AI API Gateway Platform"
LABEL org.opencontainers.image.source="https://github.com/Wei-Shaw/sub2api"
# Install runtime dependencies
RUN apk add --no-cache \
ca-certificates \
tzdata \
curl \
su-exec \
&& rm -rf /var/cache/apk/*
# Create non-root user
RUN addgroup -g 1000 sub2api && \
adduser -u 1000 -G sub2api -s /bin/sh -D sub2api
# Set working directory
WORKDIR /app
# Copy binary and runtime fallback resources from builder
COPY --from=backend-builder /app/sub2api /app/sub2api
COPY --from=backend-builder --chown=sub2api:sub2api /app/backend/resources /app/resources
# Create data directory
RUN mkdir -p /app/data && chown -R sub2api:sub2api /app
# Copy entrypoint script (fixes volume permissions then drops to sub2api)
COPY deploy/docker-entrypoint.sh /app/docker-entrypoint.sh
RUN chmod +x /app/docker-entrypoint.sh
# Expose port (can be overridden by SERVER_PORT env var)
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD wget -q -T 5 -O /dev/null http://localhost:${SERVER_PORT:-8080}/health || exit 1
# Run the application (entrypoint fixes /app/data ownership then execs as sub2api)
ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["/app/sub2api"]
+209
View File
@@ -0,0 +1,209 @@
# Edge and HTTP Ingress Security
Sub2API supports long-lived SSE and WebSocket requests. Protect the request
ingress without imposing a response `WriteTimeout`: a write deadline would
terminate healthy long generations and streams.
## Application defaults
- `server.max_header_bytes: 65536` limits HTTP/1 request headers to 64 KiB;
Go maps it to the corresponding HTTP/2 header-list limit.
- `server.read_header_timeout: 10` bounds slow-header attacks. It does not
limit request processing or response streaming.
- `server.max_request_body_size: 268435456` is the absolute 256 MiB safety net.
- `gateway.max_body_size: 268435456` remains available to multimodal, Gemini,
image, video, and batch-image endpoints.
- `gateway.text_max_body_size: 33554432` limits the known pure-text
`/embeddings` and `/alpha/search` endpoints to 32 MiB.
- H2C defaults to 50 concurrent streams per connection, a 2 MiB connection
upload window, and a 512 KiB stream upload window.
- Invalid credential abuse is limited in process by trusted client IP (IPv6
`/64`): 120 failures per 60 seconds followed by a 60-second block. This is a
per-instance safety net; multi-instance enforcement still belongs at the
load balancer, CDN, or WAF.
Do not add a single application-wide request semaphore: an SSE request may
legitimately occupy it for many minutes. Apply connection and unauthenticated
request controls at the edge; authenticated user/API-key concurrency remains
the application's responsibility.
## Trusted client IPs
`security.trust_forwarded_ip_for_api_key_acl` is enabled by default for upgrade
compatibility. While enabled, raw forwarding headers take over client-IP
resolution for logs and security-sensitive paths. Custom headers from
`security.forwarded_client_ip_headers` are checked in configured order before
the built-in `CF-Connecting-IP`, `X-Real-IP`, and `X-Forwarded-For` fallback.
Header names are case-insensitive, normalized when loaded, de-duplicated, and
limited to 16 unique valid HTTP field names. Header values must contain IP
literals; comma-separated values are supported, invalid entries are skipped,
and public addresses are preferred over private fallback addresses.
The list can be supplied in YAML or with the comma-separated environment
variable `SECURITY_FORWARDED_CLIENT_IP_HEADERS`; an explicitly empty environment
value clears YAML values. It is also editable from the admin security settings
and updates at runtime without a restart. A request snapshots the switch and
header list together, so one request cannot mix old and new settings. Custom
headers are ignored completely when the switch is disabled. In that mode Gin's
`server.trusted_proxies` chain is authoritative: configure only the exact
CIDR/IP addresses that connect directly to Sub2API. An explicit empty list
trusts no forwarded client IPs.
On the first upgrade to this mode, a legacy `false` value is changed to `true`
only when `server.trusted_proxies` was not explicitly configured; explicit
proxy policies remain in secure mode. New installations persist the configured
custom header list during database initialization. Existing installations
backfill a missing database value from the YAML configuration. A hidden
migration marker prevents later administrator changes from being overwritten.
If settings cannot be read or the persisted custom-header list is malformed,
the process fails closed to trusted-proxy mode with no custom headers. If a
migration write fails, the computed mode remains active for the current process
and startup records a warning.
Compatibility takeover accepts forwarded headers without validating the direct
peer, including any configured custom header. Protect the origin from direct
access while it is enabled. A CDN deployment must firewall the origin so only
the CDN or load balancer can reach it, and that proxy must overwrite every
trusted client-IP header rather than append an untrusted client value.
Example for a proxy on the same host:
```yaml
server:
trusted_proxies:
- 127.0.0.1/32
- ::1/128
```
## Nginx baseline
Define shared zones in the `http` block. Tune rates to measured legitimate
traffic; the values below are conservative starting points, not universal
capacity targets.
```nginx
limit_conn_zone $binary_remote_addr zone=sub2api_conn:20m;
limit_req_zone $binary_remote_addr zone=sub2api_auth:20m rate=5r/s;
limit_req_zone $binary_remote_addr zone=sub2api_api:40m rate=30r/s;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl http2;
server_name api.example.com;
client_header_timeout 10s;
client_max_body_size 256m;
large_client_header_buffers 4 16k;
limit_conn sub2api_conn 40;
location ~ ^/(auth|api/auth)/ {
limit_req zone=sub2api_auth burst=10 nodelay;
proxy_pass http://127.0.0.1:8080;
}
location ~ ^/(v1/)?(embeddings|alpha/search)$ {
client_max_body_size 32m;
limit_req zone=sub2api_api burst=60 nodelay;
proxy_pass http://127.0.0.1:8080;
}
location / {
limit_req zone=sub2api_api burst=60 nodelay;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 1800s;
proxy_send_timeout 1800s;
proxy_pass http://127.0.0.1:8080;
}
}
```
If Nginx gzip is enabled in the `http` block, keep `text/event-stream` out of
`gzip_types` and do not use `gzip_types *` for Sub2API. The
`proxy_buffering off` setting above prevents proxy buffering, but it does not
disable the gzip response filter. Use an explicit list for ordinary responses:
```nginx
gzip on;
gzip_types text/plain text/css application/json application/javascript application/xml image/svg+xml;
```
If a shared global configuration cannot exclude SSE by content type, set
`gzip off;` in the locations serving streaming API routes. This leaves gzip
available for the web UI and static assets.
Do not use an incoming `$http_x_forwarded_for` value unless Nginx real-IP
processing is restricted to explicit trusted proxy CIDRs.
## Caddy and CDN
The bundled `deploy/Caddyfile` sets a 64 KiB header limit, a 10-second header
timeout, a 256 MiB absolute body limit, and overwrites forwarded addresses from
the TCP peer. It is therefore a direct-to-Caddy baseline. Do not use its
`{remote_host}` forwarding lines unchanged behind a CDN: all clients would be
attributed to a CDN egress address, collapsing rejection aggregation and the
invalid-auth limiter onto unrelated users.
The bundled Caddy configuration leaves `flush_interval` unset so Caddy can
automatically flush `text/event-stream` responses while still propagating
client cancellation upstream. Do not set it globally: positive values can add
streaming latency, while Caddy 2.6.2's special `-1` mode also causes
reverse-proxied requests to continue after clients disconnect. The
configuration uses an explicit response content-type list for compression. Do
not replace that list with `text/*` or the shorthand `encode gzip zstd`: both
match `text/event-stream` and can buffer SSE until the response ends. Keep
streaming responses uncompressed while retaining compression for the web UI,
JSON, and static assets.
For a CDN deployment, first firewall the origin so only current CDN egress
CIDRs can connect. Then configure those exact ranges as Caddy trusted proxies
and derive upstream headers from Caddy's parsed `{client_ip}`. For example:
```caddyfile
{
servers {
trusted_proxies static 192.0.2.0/24 2001:db8:1234::/48
trusted_proxies_strict
client_ip_headers CF-Connecting-IP X-Forwarded-For
}
}
api.example.com {
reverse_proxy 127.0.0.1:8080 {
header_up X-Real-IP {client_ip}
header_up X-Forwarded-For {client_ip}
}
}
```
Replace the documentation ranges with the CDN's published, automatically
maintained egress ranges. `CF-Connecting-IP` is safe here only because direct
origin access is blocked and Caddy trusts only those TCP peers. Configure
Sub2API `server.trusted_proxies` with the Caddy address/private subnet so the
application accepts only Caddy's rewritten headers.
Caddy core does not provide a general request-rate limiter; use a trusted
CDN/WAF, a supported rate-limit module, or host firewall controls.
At a CDN/WAF, configure connection limits, header/body limits, bot challenges,
and per-IP/ASN rates before traffic reaches the origin. Allow origin ingress
only from CDN egress CIDRs or a private load balancer. Keep the application port
off the public Internet.
## DDoS boundary
Application checks reduce amplification after a connection reaches Go. They
cannot absorb volumetric attacks, TLS floods, bandwidth saturation, or a large
distributed source set. Those require upstream network capacity, CDN/WAF
filtering, provider firewall rules, and origin isolation. Avoid high-cardinality
metrics or per-request database security logs during rejection storms.
+44
View File
@@ -0,0 +1,44 @@
.PHONY: wire build build-embed test-unit test-integration test-e2e test-cover-integration
VERSION ?= $(shell ./scripts/resolve-version.sh)
LDFLAGS ?= -s -w -X main.Version=$(VERSION)
wire:
@echo "生成 Wire 代码..."
@cd cmd/server && go generate
@echo "Wire 代码生成完成"
build:
@echo "构建后端(不嵌入前端)..."
@go build -ldflags="$(LDFLAGS)" -o bin/server ./cmd/server
@echo "构建完成: bin/server"
build-embed:
@echo "构建后端(嵌入前端)..."
@go build -tags embed -ldflags="$(LDFLAGS)" -o bin/server ./cmd/server
@echo "构建完成: bin/server (with embedded frontend)"
test-unit:
@go test -tags unit ./... -count=1
test-integration:
@go test -tags integration ./... -count=1 -race -parallel=8
test-e2e:
@echo "运行 E2E 测试(需要本地服务器运行)..."
@go test -tags e2e ./internal/integration/... -count=1 -v
test-cover-integration:
@echo "运行集成测试并生成覆盖率报告..."
@go test -tags=integration -cover -coverprofile=coverage.out -count=1 -race -parallel=8 ./...
@go tool cover -func=coverage.out | tail -1
@go tool cover -html=coverage.out -o coverage.html
@echo "覆盖率报告已生成: coverage.html"
clean-coverage:
@rm -f coverage.out coverage.html
@echo "覆盖率文件已清理"
clean: clean-coverage
@rm -rf bin/
@echo "构建产物已清理"
+636
View File
@@ -0,0 +1,636 @@
# Sub2API Deployment Files
This directory contains files for deploying Sub2API on Linux servers and Apple-silicon Macs.
## Deployment Methods
| Method | Best For | Setup Wizard |
|--------|----------|--------------|
| **Docker Compose** | Quick setup, all-in-one | Not needed (auto-setup) |
| **Apple container** | Native local stack on macOS 26 | Not needed (auto-setup) |
| **Binary Install** | Production servers, systemd | Web-based wizard |
## Files
| File | Description |
|------|-------------|
| `docker-compose.yml` | Docker Compose configuration (named volumes) |
| `docker-compose.local.yml` | Docker Compose configuration (local directories, easy migration) |
| `docker-deploy.sh` | **One-click Docker deployment script (recommended)** |
| `apple-container.sh` | Native Apple `container` lifecycle script |
| `APPLE_CONTAINER.md` | Apple `container` deployment and operations guide |
| `.env.example` | Container environment variables template |
| `DOCKER.md` | Docker Hub documentation |
| `install.sh` | One-click binary installation script |
| `install-datamanagementd.sh` | datamanagementd 一键安装脚本 |
| `sub2api.service` | Systemd service unit file |
| `sub2api-datamanagementd.service` | datamanagementd systemd service unit file |
| `DATAMANAGEMENTD_CN.md` | datamanagementd 部署与联动说明(中文) |
| `config.example.yaml` | Example configuration file |
| `EDGE_SECURITY.md` | Reverse proxy, CDN/WAF, trusted proxy, and ingress hardening guide |
---
## Apple container Deployment
Apple-silicon Macs running macOS 26 can run the complete Sub2API, PostgreSQL, and Redis stack with Apple `container` 1.1.0 or newer:
```bash
./apple-container.sh init
./apple-container.sh up
./apple-container.sh status
./apple-container.sh logs app -f
```
The script uses Apple named volumes, starts dependencies in order, and performs live readiness checks. It does not provide a continuous restart supervisor; run `./apple-container.sh up` after a host reboot. Docker Compose remains the recommended production deployment path.
See [APPLE_CONTAINER.md](./APPLE_CONTAINER.md) for configuration, upgrades, persistence, networking behavior, and limitations.
---
## Docker Deployment (Recommended)
### Method 1: One-Click Deployment (Recommended)
Use the automated preparation script for the easiest setup:
```bash
# Download and run the preparation script
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/docker-deploy.sh | bash
# Or download first, then run
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/docker-deploy.sh -o docker-deploy.sh
chmod +x docker-deploy.sh
./docker-deploy.sh
```
**What the script does:**
- Downloads `docker-compose.local.yml` and `.env.example`
- Automatically generates secure secrets (JWT_SECRET, TOTP_ENCRYPTION_KEY, POSTGRES_PASSWORD)
- Creates `.env` file with generated secrets
- Creates necessary data directories (data/, postgres_data/, redis_data/)
- **Displays generated credentials** (POSTGRES_PASSWORD, JWT_SECRET, etc.)
**After running the script:**
```bash
# Start services
docker compose -f docker-compose.local.yml up -d
# View logs
docker compose -f docker-compose.local.yml logs -f sub2api
# If admin password was auto-generated, find it in logs:
docker compose -f docker-compose.local.yml logs sub2api | grep "admin password"
# Access Web UI
# http://localhost:8080
```
### Method 2: Manual Deployment
If you prefer manual control:
```bash
# Clone repository
git clone https://github.com/Wei-Shaw/sub2api.git
cd sub2api/deploy
# Configure environment
cp .env.example .env
chmod 600 .env
nano .env # Set POSTGRES_PASSWORD and other required variables
# Generate secure secrets (recommended)
JWT_SECRET=$(openssl rand -hex 32)
TOTP_ENCRYPTION_KEY=$(openssl rand -hex 32)
echo "JWT_SECRET=${JWT_SECRET}" >> .env
echo "TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY}" >> .env
# Create data directories
mkdir -p data postgres_data redis_data
# Start all services using local directory version
docker compose -f docker-compose.local.yml up -d
# View logs (check for auto-generated admin password)
docker compose -f docker-compose.local.yml logs -f sub2api
# Access Web UI
# http://localhost:8080
```
### Deployment Version Comparison
| Version | Data Storage | Migration | Best For |
|---------|-------------|-----------|----------|
| **docker-compose.local.yml** | Local directories (./data, ./postgres_data, ./redis_data) | ✅ Easy (tar entire directory) | Production, need frequent backups/migration |
| **docker-compose.yml** | Named volumes (/var/lib/docker/volumes/) | ⚠️ Requires docker commands | Simple setup, don't need migration |
**Recommendation:** Use `docker-compose.local.yml` (deployed by `docker-deploy.sh`) for easier data management and migration.
### How Auto-Setup Works
When using Docker Compose with `AUTO_SETUP=true`:
1. On first run, the system automatically:
- Connects to PostgreSQL and Redis
- Applies database migrations (SQL files in `backend/migrations/*.sql`) and records them in `schema_migrations`
- Generates JWT secret (if not provided)
- Creates admin account (password auto-generated if not provided)
- Writes config.yaml
2. No manual Setup Wizard needed - just configure `.env` and start
3. If `ADMIN_PASSWORD` is not set, check logs for the generated password:
```bash
docker compose logs sub2api | grep "admin password"
```
### Database Migration Notes (PostgreSQL)
- Migrations are applied in lexicographic order (e.g. `001_...sql`, `002_...sql`).
- `schema_migrations` tracks applied migrations (filename + checksum).
- Migrations are forward-only; rollback requires a DB backup restore or a manual compensating SQL script.
**Verify `users.allowed_groups` → `user_allowed_groups` backfill**
During the incremental GORM→Ent migration, `users.allowed_groups` (legacy `BIGINT[]`) is being replaced by a normalized join table `user_allowed_groups(user_id, group_id)`.
Run this query to compare the legacy data vs the join table:
```sql
WITH old_pairs AS (
SELECT DISTINCT u.id AS user_id, x.group_id
FROM users u
CROSS JOIN LATERAL unnest(u.allowed_groups) AS x(group_id)
WHERE u.allowed_groups IS NOT NULL
)
SELECT
(SELECT COUNT(*) FROM old_pairs) AS old_pair_count,
(SELECT COUNT(*) FROM user_allowed_groups) AS new_pair_count;
```
### datamanagementd(数据管理)联动
如需启用管理后台“数据管理”功能,请额外部署宿主机 `datamanagementd`
- 主进程固定探测 `/tmp/sub2api-datamanagement.sock`
- Docker 场景下需把宿主机 Socket 挂载到容器内同路径
- 详细步骤见:`deploy/DATAMANAGEMENTD_CN.md`
### Commands
For **local directory version** (docker-compose.local.yml):
```bash
# Start services
docker compose -f docker-compose.local.yml up -d
# Stop services
docker compose -f docker-compose.local.yml down
# View logs
docker compose -f docker-compose.local.yml logs -f sub2api
# Restart Sub2API only
docker compose -f docker-compose.local.yml restart sub2api
# Update to latest version
docker compose -f docker-compose.local.yml pull
docker compose -f docker-compose.local.yml up -d
# Remove all data (caution!)
docker compose -f docker-compose.local.yml down
rm -rf data/ postgres_data/ redis_data/
```
For **named volumes version** (docker-compose.yml):
```bash
# Start services
docker compose up -d
# Stop services
docker compose down
# View logs
docker compose logs -f sub2api
# Restart Sub2API only
docker compose restart sub2api
# Update to latest version
docker compose pull
docker compose up -d
# Remove all data (caution!)
docker compose down -v
```
### Environment Variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `POSTGRES_PASSWORD` | **Yes** | - | PostgreSQL password |
| `JWT_SECRET` | **Recommended** | *(auto-generated)* | JWT secret (fixed for persistent sessions) |
| `TOTP_ENCRYPTION_KEY` | **Recommended** | *(auto-generated)* | TOTP encryption key (fixed for persistent 2FA) |
| `SERVER_PORT` | No | `8080` | Server port |
| `ADMIN_EMAIL` | No | `admin@sub2api.local` | Admin email |
| `ADMIN_PASSWORD` | No | *(auto-generated)* | Admin password |
| `TZ` | No | `Asia/Shanghai` | Timezone |
| `UPDATE_GITHUB_TOKEN` | No | *(empty)* | Token for `api.github.com` release checks only; asset downloads remain anonymous. |
| `GEMINI_OAUTH_CLIENT_ID` | No | *(builtin)* | Google OAuth client ID (Gemini OAuth). Leave empty to use the built-in Gemini CLI client. |
| `GEMINI_OAUTH_CLIENT_SECRET` | No | *(builtin)* | Google OAuth client secret (Gemini OAuth). Leave empty to use the built-in Gemini CLI client. |
| `GEMINI_OAUTH_SCOPES` | No | *(default)* | OAuth scopes (Gemini OAuth) |
| `GEMINI_QUOTA_POLICY` | No | *(empty)* | JSON overrides for Gemini local quota simulation (Code Assist only). |
See `.env.example` for all available options.
> **Note:** The `docker-deploy.sh` script automatically generates `JWT_SECRET`, `TOTP_ENCRYPTION_KEY`, and `POSTGRES_PASSWORD` for you.
### Easy Migration (Local Directory Version)
When using `docker-compose.local.yml`, all data is stored in local directories, making migration simple:
```bash
# On source server: Stop services and create archive
cd /path/to/deployment
docker compose -f docker-compose.local.yml down
cd ..
tar czf sub2api-complete.tar.gz deployment/
# Transfer to new server
scp sub2api-complete.tar.gz user@new-server:/path/to/destination/
# On new server: Extract and start
tar xzf sub2api-complete.tar.gz
cd deployment/
docker compose -f docker-compose.local.yml up -d
```
Your entire deployment (configuration + data) is migrated!
---
## Gemini OAuth Configuration
Sub2API supports three methods to connect to Gemini:
### Method 1: Code Assist OAuth (Recommended for GCP Users)
**No configuration needed** - always uses the built-in Gemini CLI OAuth client (public).
1. Leave `GEMINI_OAUTH_CLIENT_ID` and `GEMINI_OAUTH_CLIENT_SECRET` empty
2. In the Admin UI, create a Gemini OAuth account and select **"Code Assist"** type
3. Complete the OAuth flow in your browser
> Note: Even if you configure `GEMINI_OAUTH_CLIENT_ID` / `GEMINI_OAUTH_CLIENT_SECRET` for AI Studio OAuth,
> Code Assist OAuth will still use the built-in Gemini CLI client.
**Requirements:**
- Google account with access to Google Cloud Platform
- A GCP project (auto-detected or manually specified)
**How to get Project ID (if auto-detection fails):**
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Click the project dropdown at the top of the page
3. Copy the Project ID (not the project name) from the list
4. Common formats: `my-project-123456` or `cloud-ai-companion-xxxxx`
### Method 2: AI Studio OAuth (For Regular Google Accounts)
Requires your own OAuth client credentials.
**Step 1: Create OAuth Client in Google Cloud Console**
1. Go to [Google Cloud Console - Credentials](https://console.cloud.google.com/apis/credentials)
2. Create a new project or select an existing one
3. **Enable the Generative Language API:**
- Go to "APIs & Services" → "Library"
- Search for "Generative Language API"
- Click "Enable"
4. **Configure OAuth Consent Screen** (if not done):
- Go to "APIs & Services" → "OAuth consent screen"
- Choose "External" user type
- Fill in app name, user support email, developer contact
- Add scopes: `https://www.googleapis.com/auth/generative-language.retriever` (and optionally `https://www.googleapis.com/auth/cloud-platform`)
- Add test users (your Google account email)
5. **Create OAuth 2.0 credentials:**
- Go to "APIs & Services" → "Credentials"
- Click "Create Credentials" → "OAuth client ID"
- Application type: **Web application** (or **Desktop app**)
- Name: e.g., "Sub2API Gemini"
- Authorized redirect URIs: Add `http://localhost:1455/auth/callback`
6. Copy the **Client ID** and **Client Secret**
7. **⚠️ Publish to Production (IMPORTANT):**
- Go to "APIs & Services" → "OAuth consent screen"
- Click "PUBLISH APP" to move from Testing to Production
- **Testing mode limitations:**
- Only manually added test users can authenticate (max 100 users)
- Refresh tokens expire after 7 days
- Users must be re-added periodically
- **Production mode:** Any Google user can authenticate, tokens don't expire
- Note: For sensitive scopes, Google may require verification (demo video, privacy policy)
**Step 2: Configure Environment Variables**
```bash
GEMINI_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com
GEMINI_OAUTH_CLIENT_SECRET=GOCSPX-your-client-secret
# 可选:如需使用 Gemini CLI 内置 OAuth ClientCode Assist / Google One
# 安全说明:本仓库不会内置该 client_secret,请在运行环境通过环境变量注入。
# GEMINI_CLI_OAUTH_CLIENT_SECRET=GOCSPX-your-built-in-secret
```
**Step 3: Create Account in Admin UI**
1. Create a Gemini OAuth account and select **"AI Studio"** type
2. Complete the OAuth flow
- After consent, your browser will be redirected to `http://localhost:1455/auth/callback?code=...&state=...`
- Copy the full callback URL (recommended) or just the `code` and paste it back into the Admin UI
### Method 3: API Key (Simplest)
1. Go to [Google AI Studio](https://aistudio.google.com/app/apikey)
2. Click "Create API key"
3. In Admin UI, create a Gemini **API Key** account
4. Paste your API key (starts with `AIza...`)
### Comparison Table
| Feature | Code Assist OAuth | AI Studio OAuth | API Key |
|---------|-------------------|-----------------|---------|
| Setup Complexity | Easy (no config) | Medium (OAuth client) | Easy |
| GCP Project Required | Yes | No | No |
| Custom OAuth Client | No (built-in) | Yes (required) | N/A |
| Rate Limits | GCP quota | Standard | Standard |
| Best For | GCP developers | Regular users needing OAuth | Quick testing |
---
## Binary Installation
For production servers using systemd.
### One-Line Installation
```bash
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash
```
### Manual Installation
1. Download the latest release from [GitHub Releases](https://github.com/Wei-Shaw/sub2api/releases)
2. Extract and copy the binary to `/opt/sub2api/`
3. Copy `sub2api.service` to `/etc/systemd/system/`
4. Run:
```bash
sudo systemctl daemon-reload
sudo systemctl enable sub2api
sudo systemctl start sub2api
```
5. Open the Setup Wizard in your browser to complete configuration
### Commands
```bash
# Install
sudo ./install.sh
# Upgrade
sudo ./install.sh upgrade
# Uninstall
sudo ./install.sh uninstall
```
### Service Management
```bash
# Start the service
sudo systemctl start sub2api
# Stop the service
sudo systemctl stop sub2api
# Restart the service
sudo systemctl restart sub2api
# Check status
sudo systemctl status sub2api
# View logs
sudo journalctl -u sub2api -f
# Enable auto-start on boot
sudo systemctl enable sub2api
```
### Configuration
#### Server Address and Port
During installation, you will be prompted to configure the server listen address and port. These settings are stored in the systemd service file as environment variables.
To change after installation:
1. Edit the systemd service:
```bash
sudo systemctl edit sub2api
```
2. Add or modify:
```ini
[Service]
Environment=SERVER_HOST=0.0.0.0
Environment=SERVER_PORT=3000
```
3. Reload and restart:
```bash
sudo systemctl daemon-reload
sudo systemctl restart sub2api
```
#### Gemini OAuth Configuration
If you need to use AI Studio OAuth for Gemini accounts, add the OAuth client credentials to the systemd service file:
1. Edit the service file:
```bash
sudo nano /etc/systemd/system/sub2api.service
```
2. Add your OAuth credentials in the `[Service]` section (after the existing `Environment=` lines):
```ini
Environment=GEMINI_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com
Environment=GEMINI_OAUTH_CLIENT_SECRET=GOCSPX-your-client-secret
```
如需使用“内置 Gemini CLI OAuth Client”(Code Assist / Google One),还需要注入:
```ini
Environment=GEMINI_CLI_OAUTH_CLIENT_SECRET=GOCSPX-your-built-in-secret
```
3. Reload and restart:
```bash
sudo systemctl daemon-reload
sudo systemctl restart sub2api
```
> **Note:** Code Assist OAuth does not require any configuration - it uses the built-in Gemini CLI client.
> See the [Gemini OAuth Configuration](#gemini-oauth-configuration) section above for detailed setup instructions.
#### Application Configuration
The main config file is at `/etc/sub2api/config.yaml` (created by Setup Wizard).
### Prerequisites
- Linux server (Ubuntu 20.04+, Debian 11+, CentOS 8+, etc.)
- PostgreSQL 14+
- Redis 6+
- systemd
### Directory Structure
```
/opt/sub2api/
├── sub2api # Main binary
├── sub2api.backup # Backup (after upgrade)
└── data/ # Runtime data
/etc/sub2api/
└── config.yaml # Configuration file
```
---
## Troubleshooting
### Docker
For **local directory version**:
```bash
# Check container status
docker compose -f docker-compose.local.yml ps
# View detailed logs
docker compose -f docker-compose.local.yml logs --tail=100 sub2api
# Check database connection
docker compose -f docker-compose.local.yml exec postgres pg_isready
# Check Redis connection
docker compose -f docker-compose.local.yml exec redis redis-cli ping
# Restart all services
docker compose -f docker-compose.local.yml restart
# Check data directories
ls -la data/ postgres_data/ redis_data/
```
For **named volumes version**:
```bash
# Check container status
docker compose ps
# View detailed logs
docker compose logs --tail=100 sub2api
# Check database connection
docker compose exec postgres pg_isready
# Check Redis connection
docker compose exec redis redis-cli ping
# Restart all services
docker compose restart
```
### Binary Install
```bash
# Check service status
sudo systemctl status sub2api
# View recent logs
sudo journalctl -u sub2api -n 50
# Check config file
sudo cat /etc/sub2api/config.yaml
# Check PostgreSQL
sudo systemctl status postgresql
# Check Redis
sudo systemctl status redis
```
### Common Issues
1. **Port already in use**: Change `SERVER_PORT` in `.env` or systemd config
2. **Database connection failed**: Check PostgreSQL is running and credentials are correct
3. **Redis connection failed**: Check Redis is running and password is correct
4. **Permission denied**: Ensure proper file ownership for binary install
---
## TLS Fingerprint Configuration
Sub2API supports TLS fingerprint simulation to make requests appear as if they come from the official Claude CLI (Node.js client).
> **💡 Tip:** Visit **[tls.sub2api.org](https://tls.sub2api.org/)** to get TLS fingerprint information for different devices and browsers.
### Default Behavior
- Built-in `claude_cli_v2` profile simulates Node.js 20.x + OpenSSL 3.x
- JA3 Hash: `1a28e69016765d92e3b381168d68922c`
- JA4: `t13d5911h1_a33745022dd6_1f22a2ca17c4`
- Profile selection: `accountID % profileCount`
### Configuration
```yaml
gateway:
tls_fingerprint:
enabled: true # Global switch
profiles:
# Simple profile (uses default cipher suites)
profile_1:
name: "Profile 1"
# Profile with custom cipher suites (use compact array format)
profile_2:
name: "Profile 2"
cipher_suites: [4866, 4867, 4865, 49199, 49195, 49200, 49196]
curves: [29, 23, 24]
point_formats: 0
# Another custom profile
profile_3:
name: "Profile 3"
cipher_suites: [4865, 4866, 4867, 49199, 49200]
curves: [29, 23, 24, 25]
```
### Profile Fields
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Display name (required) |
| `cipher_suites` | []uint16 | Cipher suites in decimal. Empty = default |
| `curves` | []uint16 | Elliptic curves in decimal. Empty = default |
| `point_formats` | []uint8 | EC point formats. Empty = default |
### Common Values Reference
**Cipher Suites (TLS 1.3):** `4865` (AES_128_GCM), `4866` (AES_256_GCM), `4867` (CHACHA20)
**Cipher Suites (TLS 1.2):** `49195`, `49196`, `49199`, `49200` (ECDHE variants)
**Curves:** `29` (X25519), `23` (P-256), `24` (P-384), `25` (P-521)
+926
View File
@@ -0,0 +1,926 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${SUB2API_ENV_FILE:-${SCRIPT_DIR}/.env}"
STACK_LABEL_KEY="org.sub2api.stack"
STACK_LABEL_VALUE="apple-container"
NETWORK_NAME="sub2api-apple"
APP_CONTAINER="sub2api-apple"
POSTGRES_CONTAINER="sub2api-apple-postgres"
REDIS_CONTAINER="sub2api-apple-redis"
APP_VOLUME="sub2api-apple-data"
POSTGRES_VOLUME="sub2api-apple-postgres-data"
REDIS_VOLUME="sub2api-apple-redis-data"
PLATFORM="linux/arm64"
TEMP_DIR=""
LOCK_DIR="${TMPDIR:-/tmp}/sub2api-apple-container.lock"
LOCK_ACQUIRED=false
APP_IMAGE=""
POSTGRES_IMAGE=""
REDIS_IMAGE=""
BIND_HOST=""
HOST_PORT=""
ACCESS_HOST=""
POSTGRES_USER=""
POSTGRES_PASSWORD=""
POSTGRES_DB=""
REDIS_PASSWORD=""
TZ_VALUE=""
POSTGRES_ADDRESS=""
REDIS_ADDRESS=""
APP_ENV_FILE=""
POSTGRES_ENV_FILE=""
POSTGRES_PROBE_ENV_FILE=""
REDIS_ENV_FILE=""
info() {
printf '[INFO] %s\n' "$*"
}
warn() {
printf '[WARN] %s\n' "$*" >&2
}
die() {
printf '[ERROR] %s\n' "$*" >&2
exit 1
}
usage() {
cat <<'EOF'
Usage: ./apple-container.sh <command> [options]
Commands:
init Create .env and generate required secrets
up [--recreate] Create and start the complete Sub2API stack
down Stop the stack and preserve all data
restart Restart the stack in dependency order
status Show container and workload health
logs <service> [-f] Show logs for app, postgres, or redis
pull Pull all stack images for linux/arm64
destroy [options] Delete stack containers and network
Destroy options:
--volumes Also delete all persistent data volumes
--yes Skip the confirmation prompt
Environment:
SUB2API_ENV_FILE Path to the deployment env file (default: deploy/.env)
EOF
}
cleanup() {
local exit_code=$?
if [[ -n "${TEMP_DIR}" && -d "${TEMP_DIR}" ]]; then
rm -rf "${TEMP_DIR}"
fi
if [[ "${LOCK_ACQUIRED}" == true && -d "${LOCK_DIR}" ]]; then
rm -f "${LOCK_DIR}/pid"
rmdir "${LOCK_DIR}" 2>/dev/null || true
fi
exit "${exit_code}"
}
acquire_lock() {
if ! mkdir "${LOCK_DIR}" 2>/dev/null; then
local owner_pid=""
if [[ -f "${LOCK_DIR}/pid" ]]; then
owner_pid="$(<"${LOCK_DIR}/pid")"
fi
if [[ "${owner_pid}" =~ ^[0-9]+$ ]] && ! kill -0 "${owner_pid}" 2>/dev/null; then
rm -rf "${LOCK_DIR}"
mkdir "${LOCK_DIR}" || die "Failed to reclaim stale operation lock."
else
die "Another Sub2API Apple container operation is already running."
fi
fi
printf '%s\n' "$$" >"${LOCK_DIR}/pid"
LOCK_ACQUIRED=true
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
trap 'exit 129' HUP
}
require_command() {
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
}
require_container_version() {
local version_output major minor
require_command container
require_command plutil
version_output="$(container --version)"
if [[ ! "${version_output}" =~ ([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then
die "Unable to parse Apple container version: ${version_output}"
fi
major="${BASH_REMATCH[1]}"
minor="${BASH_REMATCH[2]}"
if (( major < 1 || (major == 1 && minor < 1) )); then
die "Apple container 1.1.0 or newer is required; found ${version_output}."
fi
}
system_is_running() {
container system status >/dev/null 2>&1
}
start_system() {
if ! system_is_running; then
info "Starting Apple container services..."
container system start --enable-kernel-install
fi
}
list_resource_ids() {
case "$1" in
container) container list --all --quiet ;;
network) container network list --quiet ;;
volume) container volume list --quiet ;;
*) die "Unknown resource type: $1" ;;
esac
}
resource_exists() {
local resource_type=$1
local resource_name=$2
local output line
if ! output="$(list_resource_ids "${resource_type}")"; then
die "Failed to list Apple container ${resource_type} resources."
fi
while IFS= read -r line; do
if [[ "${line}" == "${resource_name}" ]]; then
return 0
fi
done <<<"${output}"
return 1
}
inspect_resource() {
case "$1" in
container) container inspect "$2" ;;
network) container network inspect "$2" ;;
volume) container volume inspect "$2" ;;
*) die "Unknown resource type: $1" ;;
esac
}
assert_resource_owned() {
local resource_type=$1
local resource_name=$2
local inspection compact
inspection="$(inspect_resource "${resource_type}" "${resource_name}" | \
plutil -extract 0.configuration.labels json -o - -)" || \
die "Failed to inspect ${resource_type} ${resource_name}."
compact="$(printf '%s' "${inspection}" | tr -d '[:space:]')"
if [[ "${compact}" != *"\"${STACK_LABEL_KEY}\":\"${STACK_LABEL_VALUE}\""* ]]; then
die "Refusing to manage existing ${resource_type} '${resource_name}' because it is not owned by this stack."
fi
}
preflight_stack_ownership() {
local resource_name
for resource_name in "${APP_CONTAINER}" "${REDIS_CONTAINER}" "${POSTGRES_CONTAINER}"; do
if resource_exists container "${resource_name}"; then
assert_resource_owned container "${resource_name}"
fi
done
if resource_exists network "${NETWORK_NAME}"; then
assert_resource_owned network "${NETWORK_NAME}"
fi
for resource_name in "${APP_VOLUME}" "${REDIS_VOLUME}" "${POSTGRES_VOLUME}"; do
if resource_exists volume "${resource_name}"; then
assert_resource_owned volume "${resource_name}"
fi
done
}
ensure_network() {
if resource_exists network "${NETWORK_NAME}"; then
assert_resource_owned network "${NETWORK_NAME}"
return
fi
info "Creating network ${NETWORK_NAME}..."
container network create \
--label "${STACK_LABEL_KEY}=${STACK_LABEL_VALUE}" \
"${NETWORK_NAME}" >/dev/null
}
ensure_volume() {
local volume_name=$1
if resource_exists volume "${volume_name}"; then
assert_resource_owned volume "${volume_name}"
return
fi
info "Creating volume ${volume_name}..."
container volume create \
--label "${STACK_LABEL_KEY}=${STACK_LABEL_VALUE}" \
"${volume_name}" >/dev/null
}
ensure_image_available() {
local image=$1
if container image inspect "${image}" >/dev/null 2>&1; then
return
fi
info "Pulling ${image}..."
container image pull --platform "${PLATFORM}" "${image}"
}
container_is_running() {
local container_name=$1
local output line
output="$(container list --quiet)" || die "Failed to list running Apple containers."
while IFS= read -r line; do
if [[ "${line}" == "${container_name}" ]]; then
return 0
fi
done <<<"${output}"
return 1
}
ensure_system() {
require_container_version
require_command curl
start_system
}
container_ipv4_address() {
local container_name=$1
local address
address="$(container inspect "${container_name}" | \
plutil -extract 0.status.networks.0.ipv4Address raw -o - -)" || \
die "Unable to read the network address for ${container_name}."
address="${address%%/*}"
[[ "${address}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || \
die "Apple container returned an invalid IPv4 address for ${container_name}: ${address}"
printf '%s\n' "${address}"
}
read_env_value() {
local key=$1
local fallback=${2-}
awk -v wanted="${key}" -v fallback="${fallback}" '
BEGIN { found = 0 }
/^[[:space:]]*#/ || /^[[:space:]]*$/ { next }
{
separator = index($0, "=")
if (separator == 0) { next }
key = substr($0, 1, separator - 1)
if (key == wanted) {
value = substr($0, separator + 1)
sub(/\r$/, "", value)
found = 1
}
}
END {
if (found) { print value }
else { print fallback }
}
' "${ENV_FILE}"
}
replace_env_value() {
local key=$1
local value=$2
local target_file=${3:-${ENV_FILE}}
local temp_file="${target_file}.tmp.$$"
awk -v wanted="${key}" -v replacement="${value}" '
BEGIN { replaced = 0 }
{
separator = index($0, "=")
key = separator == 0 ? "" : substr($0, 1, separator - 1)
if (key == wanted) {
if (!replaced) { print wanted "=" replacement }
replaced = 1
next
}
print
}
END {
if (!replaced) { print wanted "=" replacement }
}
' "${target_file}" >"${temp_file}"
chmod 600 "${temp_file}"
mv "${temp_file}" "${target_file}"
}
generate_secret() {
openssl rand -hex 32
}
cmd_init() {
local env_dir temp_file postgres_secret jwt_secret totp_secret
require_command openssl
if [[ -e "${ENV_FILE}" ]]; then
die "Environment file already exists: ${ENV_FILE}"
fi
postgres_secret="$(generate_secret)" || die "Failed to generate PostgreSQL password."
jwt_secret="$(generate_secret)" || die "Failed to generate JWT secret."
totp_secret="$(generate_secret)" || die "Failed to generate TOTP encryption key."
[[ -n "${postgres_secret}" && -n "${jwt_secret}" && -n "${totp_secret}" ]] || \
die "Secret generation returned an empty value."
env_dir="$(dirname "${ENV_FILE}")"
temp_file="${ENV_FILE}.init.tmp.$$"
mkdir -p "${env_dir}"
cp "${SCRIPT_DIR}/.env.example" "${temp_file}"
chmod 600 "${temp_file}"
replace_env_value POSTGRES_PASSWORD "${postgres_secret}" "${temp_file}"
replace_env_value JWT_SECRET "${jwt_secret}" "${temp_file}"
replace_env_value TOTP_ENCRYPTION_KEY "${totp_secret}" "${temp_file}"
mv "${temp_file}" "${ENV_FILE}"
info "Created ${ENV_FILE} with generated secrets."
info "Review the file, then run: SUB2API_ENV_FILE='${ENV_FILE}' ${SCRIPT_DIR}/apple-container.sh up"
}
validate_port() {
local port=$1
local decimal_port
[[ "${port}" =~ ^[0-9]+$ ]] || die "SERVER_PORT must be numeric: ${port}"
decimal_port=$((10#${port}))
(( decimal_port >= 1025 && decimal_port <= 65535 )) || \
die "SERVER_PORT must be between 1025 and 65535 for Apple container port forwarding."
}
validate_ipv4_address() {
local address=$1
local first second third fourth extra octet
IFS=. read -r first second third fourth extra <<<"${address}"
[[ -n "${first}" && -n "${second}" && -n "${third}" && -n "${fourth}" && -z "${extra}" ]] || \
die "BIND_HOST must be a valid IPv4 address: ${address}"
for octet in "${first}" "${second}" "${third}" "${fourth}"; do
[[ "${octet}" =~ ^[0-9]+$ ]] || die "BIND_HOST must be a valid IPv4 address: ${address}"
(( 10#${octet} <= 255 )) || die "BIND_HOST must be a valid IPv4 address: ${address}"
done
}
validate_env_file_security() {
local owner mode permissions
[[ -f "${ENV_FILE}" ]] || die "Environment file not found: ${ENV_FILE}. Run '$0 init' first."
owner="$(stat -f '%u' "${ENV_FILE}")" || die "Unable to read owner for ${ENV_FILE}."
mode="$(stat -f '%Lp' "${ENV_FILE}")" || die "Unable to read permissions for ${ENV_FILE}."
[[ "${owner}" == "${EUID}" ]] || die "Environment file must be owned by the current user: ${ENV_FILE}"
[[ "${mode}" =~ ^[0-7]+$ ]] || die "Unable to parse permissions for ${ENV_FILE}: ${mode}"
permissions=$((8#${mode}))
(( (permissions & 077) == 0 )) || \
die "Environment file must not be readable by group or others. Run: chmod 600 '${ENV_FILE}'"
}
prepare_environment() {
validate_env_file_security
APP_IMAGE="$(read_env_value APPLE_CONTAINER_SUB2API_IMAGE weishaw/sub2api:latest)"
POSTGRES_IMAGE="$(read_env_value APPLE_CONTAINER_POSTGRES_IMAGE postgres:18-alpine)"
REDIS_IMAGE="$(read_env_value APPLE_CONTAINER_REDIS_IMAGE redis:8-alpine)"
BIND_HOST="$(read_env_value BIND_HOST 0.0.0.0)"
HOST_PORT="$(read_env_value SERVER_PORT 8080)"
POSTGRES_USER="$(read_env_value POSTGRES_USER sub2api)"
POSTGRES_PASSWORD="$(read_env_value POSTGRES_PASSWORD)"
POSTGRES_DB="$(read_env_value POSTGRES_DB sub2api)"
REDIS_PASSWORD="$(read_env_value REDIS_PASSWORD)"
TZ_VALUE="$(read_env_value TZ Asia/Shanghai)"
[[ -n "${BIND_HOST}" ]] || die "BIND_HOST must not be empty."
validate_ipv4_address "${BIND_HOST}"
validate_port "${HOST_PORT}"
if [[ "${BIND_HOST}" == "0.0.0.0" ]]; then
ACCESS_HOST="127.0.0.1"
else
ACCESS_HOST="${BIND_HOST}"
fi
[[ -n "${POSTGRES_USER}" ]] || die "POSTGRES_USER must not be empty."
[[ -n "${POSTGRES_DB}" ]] || die "POSTGRES_DB must not be empty."
if [[ -z "${POSTGRES_PASSWORD}" || "${POSTGRES_PASSWORD}" == "change_this_secure_password" ]]; then
die "Set a secure POSTGRES_PASSWORD in ${ENV_FILE}."
fi
TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/sub2api-apple.XXXXXX")"
APP_ENV_FILE="${TEMP_DIR}/app.env"
POSTGRES_ENV_FILE="${TEMP_DIR}/postgres.env"
POSTGRES_PROBE_ENV_FILE="${TEMP_DIR}/postgres-probe.env"
REDIS_ENV_FILE="${TEMP_DIR}/redis.env"
cat >"${POSTGRES_ENV_FILE}" <<EOF
POSTGRES_USER=${POSTGRES_USER}
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
POSTGRES_DB=${POSTGRES_DB}
TZ=${TZ_VALUE}
EOF
cat >"${POSTGRES_PROBE_ENV_FILE}" <<EOF
PGPASSWORD=${POSTGRES_PASSWORD}
EOF
cat >"${REDIS_ENV_FILE}" <<EOF
REDIS_PASSWORD=${REDIS_PASSWORD}
TZ=${TZ_VALUE}
EOF
if [[ -n "${REDIS_PASSWORD}" ]]; then
printf 'REDISCLI_AUTH=%s\n' "${REDIS_PASSWORD}" >>"${REDIS_ENV_FILE}"
fi
chmod 600 "${POSTGRES_ENV_FILE}" "${POSTGRES_PROBE_ENV_FILE}" "${REDIS_ENV_FILE}"
}
prepare_app_environment() {
[[ -n "${POSTGRES_ADDRESS}" && -n "${REDIS_ADDRESS}" ]] || \
die "Dependency network addresses are not available."
cp "${ENV_FILE}" "${APP_ENV_FILE}"
cat >>"${APP_ENV_FILE}" <<EOF
AUTO_SETUP=true
SERVER_HOST=0.0.0.0
SERVER_PORT=8080
DATABASE_HOST=${POSTGRES_ADDRESS}
DATABASE_PORT=5432
DATABASE_USER=${POSTGRES_USER}
DATABASE_PASSWORD=${POSTGRES_PASSWORD}
DATABASE_DBNAME=${POSTGRES_DB}
DATABASE_SSLMODE=disable
REDIS_HOST=${REDIS_ADDRESS}
REDIS_PORT=6379
REDIS_PASSWORD=${REDIS_PASSWORD}
DATA_DIR=/app/storage/data
EOF
chmod 600 "${APP_ENV_FILE}"
}
create_postgres_container() {
info "Creating PostgreSQL container..."
container create \
--name "${POSTGRES_CONTAINER}" \
--label "${STACK_LABEL_KEY}=${STACK_LABEL_VALUE}" \
--network "${NETWORK_NAME}" \
--platform "${PLATFORM}" \
--ulimit nofile=100000:100000 \
--env-file "${POSTGRES_ENV_FILE}" \
--volume "${POSTGRES_VOLUME}:/var/lib/postgresql" \
"${POSTGRES_IMAGE}" >/dev/null
}
create_redis_container() {
info "Creating Redis container..."
container create \
--name "${REDIS_CONTAINER}" \
--label "${STACK_LABEL_KEY}=${STACK_LABEL_VALUE}" \
--network "${NETWORK_NAME}" \
--platform "${PLATFORM}" \
--ulimit nofile=100000:100000 \
--env-file "${REDIS_ENV_FILE}" \
--volume "${REDIS_VOLUME}:/var/lib/redis" \
"${REDIS_IMAGE}" \
sh -c 'set -e; mkdir -p /var/lib/redis/data; chown redis:redis /var/lib/redis/data; exec /usr/local/bin/docker-entrypoint.sh redis-server --dir /var/lib/redis/data --save 60 1 --appendonly yes --appendfsync everysec ${REDIS_PASSWORD:+--requirepass "$REDIS_PASSWORD"}' \
>/dev/null
}
create_app_container() {
info "Creating Sub2API container..."
container create \
--name "${APP_CONTAINER}" \
--label "${STACK_LABEL_KEY}=${STACK_LABEL_VALUE}" \
--network "${NETWORK_NAME}" \
--platform "${PLATFORM}" \
--ulimit nofile=100000:100000 \
--publish "${BIND_HOST}:${HOST_PORT}:8080/tcp" \
--env-file "${APP_ENV_FILE}" \
--volume "${APP_VOLUME}:/app/storage" \
--entrypoint /bin/sh \
"${APP_IMAGE}" \
-c 'set -e; mkdir -p "$DATA_DIR"; chown -R sub2api:sub2api "$DATA_DIR"; exec su-exec sub2api /app/sub2api' \
>/dev/null
}
ensure_container() {
local container_name=$1
local create_function=$2
if resource_exists container "${container_name}"; then
assert_resource_owned container "${container_name}"
return
fi
"${create_function}"
}
start_container_if_needed() {
local container_name=$1
if container_is_running "${container_name}"; then
return
fi
info "Starting ${container_name}..."
container start "${container_name}" >/dev/null
}
stop_container_if_running() {
local container_name=$1
if ! resource_exists container "${container_name}"; then
return
fi
assert_resource_owned container "${container_name}"
if container_is_running "${container_name}"; then
info "Stopping ${container_name}..."
container stop --time 30 "${container_name}" >/dev/null
fi
}
delete_container_if_present() {
local container_name=$1
if ! resource_exists container "${container_name}"; then
return
fi
assert_resource_owned container "${container_name}"
if container_is_running "${container_name}"; then
container stop --time 30 "${container_name}" >/dev/null
fi
info "Deleting ${container_name}..."
container delete "${container_name}" >/dev/null
}
wait_for_probe() {
local description=$1
local attempts=$2
shift 2
local attempt
for ((attempt = 1; attempt <= attempts; attempt++)); do
if "$@" >/dev/null 2>&1; then
info "${description} is ready."
return 0
fi
sleep 1
done
return 1
}
probe_postgres() {
container exec --env-file "${POSTGRES_PROBE_ENV_FILE}" \
"${POSTGRES_CONTAINER}" \
psql -h 127.0.0.1 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \
-v ON_ERROR_STOP=1 -tAc 'SELECT 1'
}
probe_redis() {
container exec --env-file "${REDIS_ENV_FILE}" \
"${REDIS_CONTAINER}" \
redis-cli ping
}
probe_app() {
container exec "${APP_CONTAINER}" \
wget -q -T 5 -O /dev/null http://localhost:8080/health
}
probe_host_app() {
curl --fail --silent --show-error --max-time 5 \
"http://${ACCESS_HOST}:${HOST_PORT}/health"
}
show_failure_logs() {
local container_name=$1
warn "Last logs from ${container_name}:"
container logs -n 50 "${container_name}" >&2 || true
}
start_dependencies() {
start_container_if_needed "${POSTGRES_CONTAINER}"
if ! wait_for_probe "PostgreSQL" 90 probe_postgres; then
show_failure_logs "${POSTGRES_CONTAINER}"
die "PostgreSQL did not become ready."
fi
start_container_if_needed "${REDIS_CONTAINER}"
if ! wait_for_probe "Redis" 60 probe_redis; then
show_failure_logs "${REDIS_CONTAINER}"
die "Redis did not become ready."
fi
}
start_app() {
start_container_if_needed "${APP_CONTAINER}"
if ! wait_for_probe "Sub2API" 180 probe_app; then
show_failure_logs "${APP_CONTAINER}"
die "Sub2API did not become ready."
fi
if ! wait_for_probe "Sub2API host port" 15 probe_host_app; then
die "Host port forwarding failed. In System Settings > Privacy & Security > Local Network, allow container-runtime-linux; restart Apple container services; then run 'apple-container.sh up' again."
fi
}
cmd_up() {
local recreate=false
if [[ $# -gt 1 || ($# -eq 1 && "${1-}" != "--recreate") ]]; then
usage
exit 2
fi
if [[ $# -eq 1 ]]; then
recreate=true
fi
ensure_system
prepare_environment
preflight_stack_ownership
ensure_network
ensure_volume "${APP_VOLUME}"
ensure_volume "${POSTGRES_VOLUME}"
ensure_volume "${REDIS_VOLUME}"
ensure_image_available "${APP_IMAGE}"
ensure_image_available "${POSTGRES_IMAGE}"
ensure_image_available "${REDIS_IMAGE}"
if [[ "${recreate}" == true ]]; then
delete_container_if_present "${APP_CONTAINER}"
delete_container_if_present "${REDIS_CONTAINER}"
delete_container_if_present "${POSTGRES_CONTAINER}"
fi
ensure_container "${POSTGRES_CONTAINER}" create_postgres_container
ensure_container "${REDIS_CONTAINER}" create_redis_container
start_dependencies
POSTGRES_ADDRESS="$(container_ipv4_address "${POSTGRES_CONTAINER}")"
REDIS_ADDRESS="$(container_ipv4_address "${REDIS_CONTAINER}")"
prepare_app_environment
# The dependency IPs may change whenever their lightweight VMs restart.
delete_container_if_present "${APP_CONTAINER}"
create_app_container
start_app
info "Sub2API is available at http://${ACCESS_HOST}:${HOST_PORT}"
}
cmd_down() {
require_container_version
if ! system_is_running; then
info "Apple container services are already stopped."
return
fi
preflight_stack_ownership
stop_container_if_running "${APP_CONTAINER}"
stop_container_if_running "${REDIS_CONTAINER}"
stop_container_if_running "${POSTGRES_CONTAINER}"
info "Sub2API stack stopped; persistent volumes were preserved."
}
cmd_restart() {
cmd_down
cmd_up
}
print_container_status() {
local service=$1
local container_name=$2
if ! resource_exists container "${container_name}"; then
printf '%-12s %s\n' "${service}" "missing"
elif container_is_running "${container_name}"; then
printf '%-12s %s\n' "${service}" "running"
else
printf '%-12s %s\n' "${service}" "stopped"
fi
}
cmd_status() {
local failed=0
require_container_version
if ! system_is_running; then
printf '%-12s %s\n' "system" "stopped"
return 1
fi
printf '%-12s %s\n' "system" "running"
preflight_stack_ownership
print_container_status app "${APP_CONTAINER}"
print_container_status postgres "${POSTGRES_CONTAINER}"
print_container_status redis "${REDIS_CONTAINER}"
if [[ -f "${ENV_FILE}" ]]; then
prepare_environment
if container_is_running "${POSTGRES_CONTAINER}" && probe_postgres >/dev/null 2>&1; then
printf '%-12s %s\n' "postgres" "healthy"
else
printf '%-12s %s\n' "postgres" "unhealthy"
failed=1
fi
if container_is_running "${REDIS_CONTAINER}" && probe_redis >/dev/null 2>&1; then
printf '%-12s %s\n' "redis" "healthy"
else
printf '%-12s %s\n' "redis" "unhealthy"
failed=1
fi
if container_is_running "${APP_CONTAINER}" && probe_app >/dev/null 2>&1; then
printf '%-12s %s\n' "app" "healthy"
else
printf '%-12s %s\n' "app" "unhealthy"
failed=1
fi
if container_is_running "${APP_CONTAINER}" && probe_host_app >/dev/null 2>&1; then
printf '%-12s %s\n' "host-port" "healthy"
else
printf '%-12s %s\n' "host-port" "unhealthy"
failed=1
fi
else
warn "Health probes require ${ENV_FILE}."
failed=1
fi
return "${failed}"
}
cmd_logs() {
local service=${1-}
local follow=${2-}
local container_name
[[ $# -ge 1 && $# -le 2 ]] || { usage; exit 2; }
if [[ -n "${follow}" && "${follow}" != "-f" && "${follow}" != "--follow" ]]; then
usage
exit 2
fi
case "${service}" in
app|sub2api) container_name="${APP_CONTAINER}" ;;
postgres) container_name="${POSTGRES_CONTAINER}" ;;
redis) container_name="${REDIS_CONTAINER}" ;;
*) die "Unknown service '${service}'. Use app, postgres, or redis." ;;
esac
require_container_version
system_is_running || die "Apple container services are not running."
resource_exists container "${container_name}" || die "Container not found: ${container_name}"
assert_resource_owned container "${container_name}"
if [[ -n "${follow}" ]]; then
container logs --follow "${container_name}"
else
container logs "${container_name}"
fi
}
cmd_pull() {
ensure_system
prepare_environment
info "Pulling ${APP_IMAGE}..."
container image pull --platform "${PLATFORM}" "${APP_IMAGE}"
info "Pulling ${POSTGRES_IMAGE}..."
container image pull --platform "${PLATFORM}" "${POSTGRES_IMAGE}"
info "Pulling ${REDIS_IMAGE}..."
container image pull --platform "${PLATFORM}" "${REDIS_IMAGE}"
}
confirm_destroy() {
local include_volumes=$1
local answer
if [[ "${include_volumes}" == true ]]; then
printf 'Delete the Sub2API stack and all persistent data? [y/N] '
else
printf 'Delete the Sub2API containers and network, preserving volumes? [y/N] '
fi
read -r answer
[[ "${answer}" == "y" || "${answer}" == "Y" ]]
}
delete_volume_if_present() {
local volume_name=$1
if resource_exists volume "${volume_name}"; then
assert_resource_owned volume "${volume_name}"
info "Deleting volume ${volume_name}..."
container volume delete "${volume_name}" >/dev/null
fi
}
cmd_destroy() {
local include_volumes=false
local assume_yes=false
local argument
for argument in "$@"; do
case "${argument}" in
--volumes) include_volumes=true ;;
--yes) assume_yes=true ;;
*) usage; exit 2 ;;
esac
done
require_container_version
start_system
preflight_stack_ownership
if [[ "${assume_yes}" != true ]] && ! confirm_destroy "${include_volumes}"; then
info "Cancelled."
return
fi
delete_container_if_present "${APP_CONTAINER}"
delete_container_if_present "${REDIS_CONTAINER}"
delete_container_if_present "${POSTGRES_CONTAINER}"
if resource_exists network "${NETWORK_NAME}"; then
assert_resource_owned network "${NETWORK_NAME}"
info "Deleting network ${NETWORK_NAME}..."
container network delete "${NETWORK_NAME}" >/dev/null
fi
if [[ "${include_volumes}" == true ]]; then
delete_volume_if_present "${APP_VOLUME}"
delete_volume_if_present "${REDIS_VOLUME}"
delete_volume_if_present "${POSTGRES_VOLUME}"
info "Sub2API stack and persistent data deleted."
else
info "Sub2API stack deleted; persistent volumes were preserved."
fi
}
main() {
local command=${1-}
if [[ $# -gt 0 ]]; then
shift
fi
case "${command}" in
init)
[[ $# -eq 0 ]] || { usage; exit 2; }
acquire_lock
cmd_init
;;
up)
acquire_lock
cmd_up "$@"
;;
down)
[[ $# -eq 0 ]] || { usage; exit 2; }
acquire_lock
cmd_down
;;
restart)
[[ $# -eq 0 ]] || { usage; exit 2; }
acquire_lock
cmd_restart
;;
status)
[[ $# -eq 0 ]] || { usage; exit 2; }
trap cleanup EXIT
cmd_status
;;
logs)
cmd_logs "$@"
;;
pull)
[[ $# -eq 0 ]] || { usage; exit 2; }
acquire_lock
cmd_pull
;;
destroy)
acquire_lock
cmd_destroy "$@"
;;
help|-h|--help)
usage
;;
*)
usage
exit 2
;;
esac
}
main "$@"
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# 本地构建镜像的快速脚本,避免在命令行反复输入构建参数。
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
docker build -t sub2api:latest \
--build-arg GOPROXY=https://goproxy.cn,direct \
--build-arg GOSUMDB=sum.golang.google.cn \
-f "${REPO_ROOT}/Dockerfile" \
"${REPO_ROOT}"
+5
View File
@@ -0,0 +1,5 @@
You are Codex, based on GPT-5. You are running as a coding agent in the Codex CLI on a user's computer.
{{ if .ExistingInstructions }}
{{ .ExistingInstructions }}
{{ end }}
File diff suppressed because it is too large Load Diff
+148
View File
@@ -0,0 +1,148 @@
# =============================================================================
# Sub2API Docker Compose - Local Development Build
# =============================================================================
# Build from local source code for testing changes.
#
# Usage:
# cd deploy
# docker compose -f docker-compose.dev.yml up --build
# =============================================================================
services:
sub2api:
build:
context: ..
dockerfile: Dockerfile
args:
NPM_CONFIG_REGISTRY: ${NPM_CONFIG_REGISTRY:-https://registry.npmmirror.com}
container_name: sub2api-dev
restart: unless-stopped
security_opt:
- no-new-privileges:true
ports:
- "${BIND_HOST:-127.0.0.1}:${SERVER_PORT:-8080}:8080"
volumes:
- ./data:/app/data:Z
environment:
- AUTO_SETUP=true
- SERVER_HOST=0.0.0.0
- SERVER_PORT=8080
- SERVER_MODE=debug
- ENABLE_SERVER_TIMING=${ENABLE_SERVER_TIMING:-false}
- RUN_MODE=${RUN_MODE:-standard}
- 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
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_USERNAME=${REDIS_USERNAME:-}
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
- REDIS_DB=${REDIS_DB:-0}
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
- JWT_SECRET=${JWT_SECRET:-}
- SETUP_MIGRATION_TIMEOUT_SECONDS=${SETUP_MIGRATION_TIMEOUT_SECONDS:-0}
- TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY:-}
- TZ=${TZ:-Asia/Shanghai}
# Local mainland-China development proxy. Containers cannot use
# 127.0.0.1 for the host proxy, so default to Docker Desktop's host name.
- HTTP_PROXY=${SUB2API_DEV_HTTP_PROXY:-http://host.docker.internal:7897}
- HTTPS_PROXY=${SUB2API_DEV_HTTPS_PROXY:-http://host.docker.internal:7897}
- ALL_PROXY=${SUB2API_DEV_ALL_PROXY:-socks5://host.docker.internal:7897}
- NO_PROXY=${SUB2API_DEV_NO_PROXY:-127.0.0.1,localhost,::1,postgres,redis,sub2api,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,.local}
# 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_OPENAI_PROXY_STREAM_CIRCUIT_FAILURE_THRESHOLD=${GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_FAILURE_THRESHOLD:-2}
- GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_WINDOW_SECONDS=${GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_WINDOW_SECONDS:-60}
- GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_TTL_SECONDS=${GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_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}
- BATCH_IMAGE_ENABLED=${BATCH_IMAGE_ENABLED:-true}
- BATCH_IMAGE_QUEUE_ENABLED=${BATCH_IMAGE_QUEUE_ENABLED:-true}
- BATCH_IMAGE_VERTEX_ENABLED=${BATCH_IMAGE_VERTEX_ENABLED:-true}
- BATCH_IMAGE_VERTEX_PROJECT_ID=${BATCH_IMAGE_VERTEX_PROJECT_ID:-project-28424c50-8df2-46e2-a27}
- BATCH_IMAGE_VERTEX_LOCATION=${BATCH_IMAGE_VERTEX_LOCATION:-global}
- BATCH_IMAGE_VERTEX_MANAGED_GCS_BUCKET=${BATCH_IMAGE_VERTEX_MANAGED_GCS_BUCKET:-sub2-batch-image-prod-project-28424c50-8df2-46e2-a27}
- BATCH_IMAGE_VERTEX_MANAGED_GCS_PREFIX=${BATCH_IMAGE_VERTEX_MANAGED_GCS_PREFIX:-batch-image/prod/{batch_id}}
- BATCH_IMAGE_VERTEX_INPUT_RETENTION_HOURS=${BATCH_IMAGE_VERTEX_INPUT_RETENTION_HOURS:-24}
- BATCH_IMAGE_VERTEX_OUTPUT_RETENTION_HOURS=${BATCH_IMAGE_VERTEX_OUTPUT_RETENTION_HOURS:-72}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- sub2api-network
healthcheck:
test: ["CMD", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
postgres:
image: postgres:18-alpine
container_name: sub2api-postgres-dev
restart: unless-stopped
volumes:
- ./postgres_data:/var/lib/postgresql/data:Z
environment:
- POSTGRES_USER=${POSTGRES_USER:-sub2api}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
- POSTGRES_DB=${POSTGRES_DB:-sub2api}
- PGDATA=/var/lib/postgresql/data
- TZ=${TZ:-Asia/Shanghai}
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: 10s
redis:
image: redis:8-alpine
container_name: sub2api-redis-dev
restart: unless-stopped
volumes:
- ./redis_data:/data:Z
# The command is one quoted script for the inner `sh -c`. Compose keeps
# the newlines inside the quoted string, so every line needs a trailing
# `\` — without it, `redis-server` on the first line runs with no flags
# at all, and the --save/--appendonly/--appendfsync lines are never read.
command: >
sh -c '
redis-server \
--save 60 1 \
--appendonly yes \
--appendfsync everysec \
${REDIS_PASSWORD:+--requirepass "$REDIS_PASSWORD"}'
environment:
- TZ=${TZ:-Asia/Shanghai}
- REDISCLI_AUTH=${REDIS_PASSWORD:-}
networks:
- sub2api-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
networks:
sub2api-network:
driver: bridge
+269
View File
@@ -0,0 +1,269 @@
# =============================================================================
# Sub2API Docker Compose - Local Directory Version
# =============================================================================
# This configuration uses local directories for data storage instead of named
# volumes, making it easy to migrate the entire deployment by simply copying
# the deploy directory.
#
# Quick Start:
# 1. Copy .env.example to .env and configure
# 2. mkdir -p data postgres_data redis_data
# 3. docker-compose -f docker-compose.local.yml up -d
# 4. Check logs: docker-compose -f docker-compose.local.yml logs -f sub2api
# 5. Access: http://localhost:8080
#
# Migration to New Server:
# 1. docker-compose -f docker-compose.local.yml down
# 2. tar czf sub2api-deploy.tar.gz deploy/
# 3. Transfer to new server and extract
# 4. docker-compose -f docker-compose.local.yml up -d
# =============================================================================
services:
# ===========================================================================
# Sub2API Application
# ===========================================================================
sub2api:
image: weishaw/sub2api:latest
container_name: sub2api
restart: unless-stopped
security_opt:
- no-new-privileges:true
ulimits:
nofile:
soft: 100000
hard: 100000
ports:
- "${BIND_HOST:-0.0.0.0}:${SERVER_PORT:-8080}:8080"
volumes:
# Local directory mapping for easy migration
- ./data:/app/data:Z
# Optional: Mount custom config.yaml (uncomment and create the file first)
# Copy config.example.yaml to config.yaml, modify it, then uncomment:
# - ./config.yaml:/app/data/config.yaml
environment:
# =======================================================================
# Auto Setup (REQUIRED for Docker deployment)
# =======================================================================
- AUTO_SETUP=true
# =======================================================================
# Server Configuration
# =======================================================================
- SERVER_HOST=0.0.0.0
- SERVER_PORT=8080
- SERVER_MODE=${SERVER_MODE:-release}
- ENABLE_SERVER_TIMING=${ENABLE_SERVER_TIMING:-false}
- RUN_MODE=${RUN_MODE:-standard}
- UPDATE_GITHUB_TOKEN=${UPDATE_GITHUB_TOKEN:-}
# =======================================================================
# Database Configuration (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 Configuration
# =======================================================================
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_USERNAME=${REDIS_USERNAME:-}
- 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 Account (auto-created on first run)
# =======================================================================
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
# =======================================================================
# JWT Configuration
# =======================================================================
# IMPORTANT: Set a fixed JWT_SECRET to prevent login sessions from being
# invalidated after container restarts. If left empty, a random secret
# will be generated on each startup.
# Generate a secure secret: openssl rand -hex 32
- JWT_SECRET=${JWT_SECRET:-}
- JWT_EXPIRE_HOUR=${JWT_EXPIRE_HOUR:-24}
# =======================================================================
# Setup Configuration
# =======================================================================
- SETUP_MIGRATION_TIMEOUT_SECONDS=${SETUP_MIGRATION_TIMEOUT_SECONDS:-0}
# =======================================================================
# TOTP (2FA) Configuration
# =======================================================================
# IMPORTANT: Set a fixed encryption key for TOTP secrets. If left empty,
# a random key will be generated on each startup, causing all existing
# TOTP configurations to become invalid (users won't be able to login
# with 2FA).
# Generate a secure key: openssl rand -hex 32
- TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY:-}
# =======================================================================
# Timezone Configuration
# This affects ALL time operations in the application:
# - Database timestamps
# - Usage statistics "today" boundary
# - Subscription expiry times
# - Log timestamps
# Common values: Asia/Shanghai, America/New_York, Europe/London, UTC
# =======================================================================
- TZ=${TZ:-Asia/Shanghai}
# =======================================================================
# Gemini OAuth Configuration (for Gemini accounts)
# =======================================================================
- 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:-}
# Built-in OAuth client secrets (optional)
# SECURITY: This repo does not embed third-party client_secret.
- 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 Configuration (URL Allowlist)
# =======================================================================
# Enable URL allowlist validation (false to skip allowlist checks)
- SECURITY_URL_ALLOWLIST_ENABLED=${SECURITY_URL_ALLOWLIST_ENABLED:-false}
# Allow insecure HTTP URLs when allowlist is disabled (default: true; set to false to require https)
- SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=${SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP:-true}
# Allow private IP addresses for upstream/pricing/CRS (default: true; set to false to block private hosts)
- SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-true}
# Upstream hosts whitelist (comma-separated, only used when enabled=true)
- SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS=${SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS:-}
# =======================================================================
# Update Configuration (在线更新配置)
# =======================================================================
# Proxy for accessing GitHub (online updates + pricing data)
# Examples: http://host:port, socks5://host:port
- UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-}
# =======================================================================
# 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_OPENAI_PROXY_STREAM_CIRCUIT_FAILURE_THRESHOLD=${GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_FAILURE_THRESHOLD:-2}
- GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_WINDOW_SECONDS=${GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_WINDOW_SECONDS:-60}
- GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_TTL_SECONDS=${GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_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
networks:
- sub2api-network
healthcheck:
test: ["CMD", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://localhost:8080/health"]
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:
# Local directory mapping for easy migration
- ./postgres_data:/var/lib/postgresql/data:Z
environment:
- POSTGRES_USER=${POSTGRES_USER:-sub2api}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
- POSTGRES_DB=${POSTGRES_DB:-sub2api}
- PGDATA=/var/lib/postgresql/data
- TZ=${TZ:-Asia/Shanghai}
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: 10s
# 注意:不暴露端口到宿主机,应用通过内部网络连接
# 如需调试,可临时添加:ports: ["127.0.0.1:5433:5432"]
# ===========================================================================
# Redis Cache
# ===========================================================================
redis:
image: redis:8-alpine
container_name: sub2api-redis
restart: unless-stopped
ulimits:
nofile:
soft: 100000
hard: 100000
volumes:
# Local directory mapping for easy migration
- ./redis_data:/data:Z
# The command is one quoted script for the inner `sh -c`. Compose keeps
# the newlines inside the quoted string, so every line needs a trailing
# `\` — without it, `redis-server` on the first line runs with no flags
# at all, and the --save/--appendonly/--appendfsync lines are never read.
command: >
sh -c '
redis-server \
--save 60 1 \
--appendonly yes \
--appendfsync everysec \
${REDIS_PASSWORD:+--requirepass "$REDIS_PASSWORD"}'
environment:
- TZ=${TZ:-Asia/Shanghai}
# REDISCLI_AUTH is used by redis-cli for authentication (safer than -a flag)
- REDISCLI_AUTH=${REDIS_PASSWORD:-}
networks:
- sub2api-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
# =============================================================================
# Networks
# =============================================================================
networks:
sub2api-network:
driver: bridge
+137
View File
@@ -0,0 +1,137 @@
# =============================================================================
# Sub2API Docker Compose - Standalone Configuration
# =============================================================================
# This configuration runs only the Sub2API application.
# PostgreSQL and Redis must be provided externally.
#
# Usage:
# 1. Copy .env.example to .env and configure database/redis connection
# 2. docker-compose -f docker-compose.standalone.yml up -d
# 3. Access: http://localhost:8080
# =============================================================================
services:
sub2api:
image: weishaw/sub2api:latest
container_name: sub2api
restart: unless-stopped
security_opt:
- no-new-privileges:true
ulimits:
nofile:
soft: 100000
hard: 100000
ports:
- "${BIND_HOST:-0.0.0.0}:${SERVER_PORT:-8080}:8080"
volumes:
- sub2api_data:/app/data
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
# =======================================================================
# Auto Setup
# =======================================================================
- AUTO_SETUP=true
# =======================================================================
# Server Configuration
# =======================================================================
- SERVER_HOST=0.0.0.0
- SERVER_PORT=8080
- SERVER_MODE=${SERVER_MODE:-release}
- ENABLE_SERVER_TIMING=${ENABLE_SERVER_TIMING:-false}
- RUN_MODE=${RUN_MODE:-standard}
- UPDATE_GITHUB_TOKEN=${UPDATE_GITHUB_TOKEN:-}
# =======================================================================
# Database Configuration (PostgreSQL) - Required
# =======================================================================
- DATABASE_HOST=${DATABASE_HOST:?DATABASE_HOST is required}
- DATABASE_PORT=${DATABASE_PORT:-5432}
- DATABASE_USER=${DATABASE_USER:-sub2api}
- DATABASE_PASSWORD=${DATABASE_PASSWORD:?DATABASE_PASSWORD is required}
- DATABASE_DBNAME=${DATABASE_DBNAME:-sub2api}
- DATABASE_SSLMODE=${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 Configuration - Required
# =======================================================================
- REDIS_HOST=${REDIS_HOST:?REDIS_HOST is required}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_USERNAME=${REDIS_USERNAME:-}
- 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 Account (auto-created on first run)
# =======================================================================
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
# =======================================================================
# JWT Configuration
# =======================================================================
- JWT_SECRET=${JWT_SECRET:-}
- JWT_EXPIRE_HOUR=${JWT_EXPIRE_HOUR:-24}
# =======================================================================
# Setup Configuration
# =======================================================================
- SETUP_MIGRATION_TIMEOUT_SECONDS=${SETUP_MIGRATION_TIMEOUT_SECONDS:-0}
# =======================================================================
# Timezone Configuration
# =======================================================================
- TZ=${TZ:-Asia/Shanghai}
# =======================================================================
# Gemini OAuth Configuration (optional)
# =======================================================================
- 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:-}
# Built-in OAuth client secrets (optional)
# SECURITY: This repo does not embed third-party client_secret.
- 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:-}
# =======================================================================
# 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_OPENAI_PROXY_STREAM_CIRCUIT_FAILURE_THRESHOLD=${GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_FAILURE_THRESHOLD:-2}
- GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_WINDOW_SECONDS=${GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_WINDOW_SECONDS:-60}
- GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_TTL_SECONDS=${GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_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}
healthcheck:
test: ["CMD", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
sub2api_data:
driver: local
+302
View File
@@ -0,0 +1,302 @@
# =============================================================================
# Sub2API Docker Compose Configuration
# =============================================================================
# Quick Start:
# 1. Copy .env.example to .env and configure
# 2. docker-compose up -d
# 3. Check logs: docker-compose logs -f sub2api
# 4. Access: http://localhost:8080
#
# All configuration is done via environment variables.
# No Setup Wizard needed - the system auto-initializes on first run.
# =============================================================================
services:
# ===========================================================================
# Sub2API Application
# ===========================================================================
sub2api:
image: weishaw/sub2api:latest
container_name: sub2api
restart: unless-stopped
security_opt:
- no-new-privileges:true
ulimits:
nofile:
soft: 100000
hard: 100000
ports:
- "${BIND_HOST:-0.0.0.0}:${SERVER_PORT:-8080}:8080"
volumes:
# Data persistence (config.yaml will be auto-generated here)
- sub2api_data:/app/data
# Optional: Mount custom config.yaml (uncomment and create the file first)
# Copy config.example.yaml to config.yaml, modify it, then uncomment:
# - ./config.yaml:/app/data/config.yaml
# Optional: Mount a custom Codex instructions template file, then point
# gateway.forced_codex_instructions_template_file at /app/data/codex-instructions.md.tmpl
# in config.yaml.
# - ./codex-instructions.md.tmpl:/app/data/codex-instructions.md.tmpl:ro
environment:
# =======================================================================
# Auto Setup (REQUIRED for Docker deployment)
# =======================================================================
- AUTO_SETUP=true
# =======================================================================
# Server Configuration
# =======================================================================
- SERVER_HOST=0.0.0.0
- SERVER_PORT=8080
- SERVER_MODE=${SERVER_MODE:-release}
- ENABLE_SERVER_TIMING=${ENABLE_SERVER_TIMING:-false}
- RUN_MODE=${RUN_MODE:-standard}
- UPDATE_GITHUB_TOKEN=${UPDATE_GITHUB_TOKEN:-}
- ALIPAY_MOBILE_PRECREATE_DEEP_LINK=${ALIPAY_MOBILE_PRECREATE_DEEP_LINK:-}
# =======================================================================
# Database Configuration (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 Configuration
# =======================================================================
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_USERNAME=${REDIS_USERNAME:-}
- 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 Account (auto-created on first run)
# =======================================================================
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
# =======================================================================
# JWT Configuration
# =======================================================================
# IMPORTANT: Set a fixed JWT_SECRET to prevent login sessions from being
# invalidated after container restarts. If left empty, a random secret
# will be generated on each startup.
# Generate a secure secret: openssl rand -hex 32
- JWT_SECRET=${JWT_SECRET:-}
- JWT_EXPIRE_HOUR=${JWT_EXPIRE_HOUR:-24}
# =======================================================================
# Setup Configuration
# =======================================================================
- SETUP_MIGRATION_TIMEOUT_SECONDS=${SETUP_MIGRATION_TIMEOUT_SECONDS:-0}
# =======================================================================
# TOTP (2FA) Configuration
# =======================================================================
# IMPORTANT: Set a fixed encryption key for TOTP secrets. If left empty,
# a random key will be generated on each startup, causing all existing
# TOTP configurations to become invalid (users won't be able to login
# with 2FA).
# Generate a secure key: openssl rand -hex 32
- TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY:-}
# =======================================================================
# Timezone Configuration
# This affects ALL time operations in the application:
# - Database timestamps
# - Usage statistics "today" boundary
# - Subscription expiry times
# - Log timestamps
# Common values: Asia/Shanghai, America/New_York, Europe/London, UTC
# =======================================================================
- TZ=${TZ:-Asia/Shanghai}
# =======================================================================
# Gemini OAuth Configuration (for Gemini accounts)
# =======================================================================
- 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:-}
# Built-in OAuth client secrets (optional)
# SECURITY: This repo does not embed third-party client_secret.
- 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 Configuration (URL Allowlist)
# =======================================================================
# Enable URL allowlist validation (false to skip allowlist checks)
- SECURITY_URL_ALLOWLIST_ENABLED=${SECURITY_URL_ALLOWLIST_ENABLED:-false}
# Allow insecure HTTP URLs when allowlist is disabled (default: true; set to false to require https)
- SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=${SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP:-true}
# Allow private IP addresses for upstream/pricing/CRS (default: true; set to false to block private hosts)
- SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-true}
# Upstream hosts whitelist (comma-separated, only used when enabled=true)
- SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS=${SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS:-}
# =======================================================================
# Update Configuration (在线更新配置)
# =======================================================================
# Proxy for accessing GitHub (online updates + pricing data)
# Examples: http://host:port, socks5://host:port
- UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-}
# =======================================================================
# 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_OPENAI_PROXY_STREAM_CIRCUIT_FAILURE_THRESHOLD=${GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_FAILURE_THRESHOLD:-2}
- GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_WINDOW_SECONDS=${GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_WINDOW_SECONDS:-60}
- GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_TTL_SECONDS=${GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_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
networks:
- sub2api-network
healthcheck:
test:
[
"CMD",
"wget",
"-q",
"-T",
"5",
"-O",
"/dev/null",
"http://localhost:8080/health",
]
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
# Pass the tuning values documented in .env.example to postgres. The
# fallbacks are the postgres:18 stock defaults, so nothing changes when
# the variables are not set.
command: >
postgres
-c max_connections=${POSTGRES_MAX_CONNECTIONS:-100}
-c shared_buffers=${POSTGRES_SHARED_BUFFERS:-128MB}
-c effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-4GB}
-c maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
# postgres:18-alpine 默认 PGDATA=/var/lib/postgresql/18/docker(位于镜像声明的匿名卷 /var/lib/postgresql 内)。
# 若不显式设置 PGDATA,则即使挂载了 postgres_data 到 /var/lib/postgresql/data,数据也不会落盘到该命名卷,
# docker compose down/up 后会触发 initdb 重新初始化,导致用户/密码等数据丢失。
- 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}
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: 10s
# 注意:不暴露端口到宿主机,应用通过内部网络连接
# 如需调试,可临时添加:ports: ["127.0.0.1:5433:5432"]
# ===========================================================================
# Redis Cache
# ===========================================================================
redis:
image: redis:8-alpine
container_name: sub2api-redis
restart: unless-stopped
ulimits:
nofile:
soft: 100000
hard: 100000
volumes:
- redis_data:/data
# The command is one quoted script for the inner `sh -c`. Compose keeps
# the newlines inside the quoted string, so every line needs a trailing
# `\` — without it, `redis-server` on the first line runs with no flags
# at all, and the --save/--appendonly/--appendfsync lines are never read.
command: >
sh -c '
redis-server \
--save 60 1 \
--appendonly yes \
--appendfsync everysec \
${REDIS_PASSWORD:+--requirepass "$REDIS_PASSWORD"}'
environment:
- TZ=${TZ:-Asia/Shanghai}
# REDISCLI_AUTH is used by redis-cli for authentication (safer than -a flag)
- REDISCLI_AUTH=${REDIS_PASSWORD:-}
networks:
- sub2api-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
# 注意:不暴露端口到宿主机,应用通过内部网络连接
# 如需调试,可临时添加:ports: ["127.0.0.1:6379:6379"]
# =============================================================================
# Volumes
# =============================================================================
volumes:
sub2api_data:
driver: local
postgres_data:
driver: local
redis_data:
driver: local
# =============================================================================
# Networks
# =============================================================================
networks:
sub2api-network:
driver: bridge
+171
View File
@@ -0,0 +1,171 @@
#!/bin/bash
# =============================================================================
# Sub2API Docker Deployment Preparation Script
# =============================================================================
# This script prepares deployment files for Sub2API:
# - Downloads docker-compose.local.yml and .env.example
# - Generates secure secrets (JWT_SECRET, TOTP_ENCRYPTION_KEY, POSTGRES_PASSWORD)
# - Creates necessary data directories
#
# After running this script, you can start services with:
# docker-compose up -d
# =============================================================================
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# GitHub raw content base URL
GITHUB_RAW_URL="https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy"
# Print colored message
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Generate random secret
generate_secret() {
openssl rand -hex 32
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Main installation function
main() {
echo ""
echo "=========================================="
echo " Sub2API Deployment Preparation"
echo "=========================================="
echo ""
# Check if openssl is available
if ! command_exists openssl; then
print_error "openssl is not installed. Please install openssl first."
exit 1
fi
# Check if deployment already exists
if [ -f "docker-compose.yml" ] && [ -f ".env" ]; then
print_warning "Deployment files already exist in current directory."
read -p "Overwrite existing files? (y/N): " -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_info "Cancelled."
exit 0
fi
fi
# Download docker-compose.local.yml and save as docker-compose.yml
print_info "Downloading docker-compose.yml..."
if command_exists curl; then
curl -sSL "${GITHUB_RAW_URL}/docker-compose.local.yml" -o docker-compose.yml
elif command_exists wget; then
wget -q "${GITHUB_RAW_URL}/docker-compose.local.yml" -O docker-compose.yml
else
print_error "Neither curl nor wget is installed. Please install one of them."
exit 1
fi
print_success "Downloaded docker-compose.yml"
# Download .env.example
print_info "Downloading .env.example..."
if command_exists curl; then
curl -sSL "${GITHUB_RAW_URL}/.env.example" -o .env.example
else
wget -q "${GITHUB_RAW_URL}/.env.example" -O .env.example
fi
print_success "Downloaded .env.example"
# Generate .env file with auto-generated secrets
print_info "Generating secure secrets..."
echo ""
# Generate secrets
JWT_SECRET=$(generate_secret)
TOTP_ENCRYPTION_KEY=$(generate_secret)
POSTGRES_PASSWORD=$(generate_secret)
# Create .env from .env.example
cp .env.example .env
# Update .env with generated secrets (cross-platform compatible)
if sed --version >/dev/null 2>&1; then
# GNU sed (Linux)
sed -i "s/^JWT_SECRET=.*/JWT_SECRET=${JWT_SECRET}/" .env
sed -i "s/^TOTP_ENCRYPTION_KEY=.*/TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY}/" .env
sed -i "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=${POSTGRES_PASSWORD}/" .env
else
# BSD sed (macOS)
sed -i '' "s/^JWT_SECRET=.*/JWT_SECRET=${JWT_SECRET}/" .env
sed -i '' "s/^TOTP_ENCRYPTION_KEY=.*/TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY}/" .env
sed -i '' "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=${POSTGRES_PASSWORD}/" .env
fi
# Create data directories
print_info "Creating data directories..."
mkdir -p data postgres_data redis_data
print_success "Created data directories"
# Set secure permissions for .env file (readable/writable only by owner)
chmod 600 .env
echo ""
# Display completion message
echo "=========================================="
echo " Preparation Complete!"
echo "=========================================="
echo ""
echo "Generated secure credentials:"
echo " POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}"
echo " JWT_SECRET: ${JWT_SECRET}"
echo " TOTP_ENCRYPTION_KEY: ${TOTP_ENCRYPTION_KEY}"
echo ""
print_warning "These credentials have been saved to .env file."
print_warning "Please keep them secure and do not share publicly!"
echo ""
echo "Directory structure:"
echo " docker-compose.yml - Docker Compose configuration"
echo " .env - Environment variables (generated secrets)"
echo " .env.example - Example template (for reference)"
echo " data/ - Application data (will be created on first run)"
echo " postgres_data/ - PostgreSQL data"
echo " redis_data/ - Redis data"
echo ""
echo "Next steps:"
echo " 1. (Optional) Edit .env to customize configuration"
echo " 2. Start services:"
echo " docker-compose up -d"
echo ""
echo " 3. View logs:"
echo " docker-compose logs -f sub2api"
echo ""
echo " 4. Access Web UI:"
echo " http://localhost:8080"
echo ""
print_info "If admin password is not set in .env, it will be auto-generated."
print_info "Check logs for the generated admin password on first startup."
echo ""
}
# Run main function
main "$@"
+23
View File
@@ -0,0 +1,23 @@
#!/bin/sh
set -e
# Fix data directory permissions when running as root.
# Docker named volumes / host bind-mounts may be owned by root,
# preventing the non-root sub2api user from writing files.
if [ "$(id -u)" = "0" ]; then
mkdir -p /app/data
# Use || true to avoid failure on read-only mounted files (e.g. config.yaml:ro)
chown -R sub2api:sub2api /app/data 2>/dev/null || true
# Re-invoke this script as sub2api so the flag-detection below
# also runs under the correct user.
exec su-exec sub2api "$0" "$@"
fi
# Compatibility: if the first arg looks like a flag (e.g. --help),
# prepend the default binary so it behaves the same as the old
# ENTRYPOINT ["/app/sub2api"] style.
if [ "${1#-}" != "$1" ]; then
set -- /app/sub2api "$@"
fi
exec "$@"
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env bash
set -euo pipefail
# 用法:
# sudo ./install-datamanagementd.sh --binary /path/to/datamanagementd
# 或:
# sudo ./install-datamanagementd.sh --source /path/to/sub2api/repo
BIN_PATH=""
SOURCE_PATH=""
INSTALL_DIR="/opt/sub2api"
DATA_DIR="/var/lib/sub2api/datamanagement"
SERVICE_FILE_NAME="sub2api-datamanagementd.service"
function print_help() {
cat <<'EOF'
用法:
install-datamanagementd.sh [--binary <datamanagementd二进制路径>] [--source <仓库路径>]
参数:
--binary 指定已构建的 datamanagementd 二进制路径
--source 指定 sub2api 仓库路径(脚本会执行 go build)
-h, --help 显示帮助
示例:
sudo ./install-datamanagementd.sh --binary ./datamanagement/datamanagementd
sudo ./install-datamanagementd.sh --source /opt/sub2api-src
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--binary)
BIN_PATH="${2:-}"
shift 2
;;
--source)
SOURCE_PATH="${2:-}"
shift 2
;;
-h|--help)
print_help
exit 0
;;
*)
echo "未知参数: $1"
print_help
exit 1
;;
esac
done
if [[ -n "$BIN_PATH" && -n "$SOURCE_PATH" ]]; then
echo "错误: --binary 与 --source 只能二选一"
exit 1
fi
if [[ -z "$BIN_PATH" && -z "$SOURCE_PATH" ]]; then
echo "错误: 必须提供 --binary 或 --source"
exit 1
fi
if [[ "$(id -u)" -ne 0 ]]; then
echo "错误: 请使用 root 权限执行(例如 sudo"
exit 1
fi
if [[ -n "$SOURCE_PATH" ]]; then
if [[ ! -d "$SOURCE_PATH/datamanagement" ]]; then
echo "错误: 无效仓库路径,未找到 $SOURCE_PATH/datamanagement"
exit 1
fi
echo "[1/6] 从源码构建 datamanagementd..."
(cd "$SOURCE_PATH/datamanagement" && go build -o datamanagementd ./cmd/datamanagementd)
BIN_PATH="$SOURCE_PATH/datamanagement/datamanagementd"
fi
if [[ ! -f "$BIN_PATH" ]]; then
echo "错误: 二进制文件不存在: $BIN_PATH"
exit 1
fi
if ! id sub2api >/dev/null 2>&1; then
echo "[2/6] 创建系统用户 sub2api..."
useradd --system --no-create-home --shell /usr/sbin/nologin sub2api
else
echo "[2/6] 系统用户 sub2api 已存在,跳过创建"
fi
echo "[3/6] 安装 datamanagementd 二进制..."
mkdir -p "$INSTALL_DIR"
install -m 0755 "$BIN_PATH" "$INSTALL_DIR/datamanagementd"
echo "[4/6] 准备数据目录..."
mkdir -p "$DATA_DIR"
chown -R sub2api:sub2api /var/lib/sub2api
chmod 0750 "$DATA_DIR"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SERVICE_TEMPLATE="$SCRIPT_DIR/$SERVICE_FILE_NAME"
if [[ ! -f "$SERVICE_TEMPLATE" ]]; then
echo "错误: 未找到服务模板 $SERVICE_TEMPLATE"
exit 1
fi
echo "[5/6] 安装 systemd 服务..."
cp "$SERVICE_TEMPLATE" "/etc/systemd/system/$SERVICE_FILE_NAME"
systemctl daemon-reload
systemctl enable --now sub2api-datamanagementd
echo "[6/6] 完成,当前状态:"
systemctl --no-pager --full status sub2api-datamanagementd || true
cat <<'EOF'
下一步建议:
1. 查看日志:sudo journalctl -u sub2api-datamanagementd -f
2. 在 sub2api(容器部署时)挂载 socket:
/tmp/sub2api-datamanagement.sock:/tmp/sub2api-datamanagement.sock
3. 进入管理后台“数据管理”页面确认 agent=enabled
EOF
+1232
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
[Unit]
Description=Sub2API Data Management Daemon
After=network.target
Wants=network.target
[Service]
Type=simple
User=sub2api
Group=sub2api
WorkingDirectory=/opt/sub2api
ExecStart=/opt/sub2api/datamanagementd \
-socket-path /tmp/sub2api-datamanagement.sock \
-sqlite-path /var/lib/sub2api/datamanagement/datamanagementd.db \
-version 1.0.0
Restart=always
RestartSec=5s
LimitNOFILE=100000
NoNewPrivileges=true
PrivateTmp=false
[Install]
WantedBy=multi-user.target
+33
View File
@@ -0,0 +1,33 @@
[Unit]
Description=Sub2API - AI API Gateway Platform
Documentation=https://github.com/Wei-Shaw/sub2api
After=network.target postgresql.service redis.service
Wants=postgresql.service redis.service
[Service]
Type=simple
User=sub2api
Group=sub2api
WorkingDirectory=/opt/sub2api
ExecStart=/opt/sub2api/sub2api
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=sub2api
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/opt/sub2api
# Environment - Server configuration
# Modify these values to change listen address and port
Environment=GIN_MODE=release
Environment=SERVER_HOST=0.0.0.0
Environment=SERVER_PORT=8080
[Install]
WantedBy=multi-user.target
+85
View File
@@ -0,0 +1,85 @@
#!/bin/sh
set -eu
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
caddyfile="$repo_root/deploy/Caddyfile"
active_config=$(sed 's/[[:space:]]*#.*$//' "$caddyfile")
normalized_config=$(printf '%s\n' "$active_config" | awk '
NF > 0 {
for (field = 1; field <= NF; field++) {
token = $field
sub(/^["`]/, "", token)
sub(/["`]$/, "", token)
printf "%s%s", (field == 1 ? "" : " "), token
}
print ""
}
')
# Keep one canonical encode block instead of reimplementing Caddy matcher semantics.
expected_encode_block=$(cat <<'EOF'
encode {
zstd
gzip 6
minimum_length 256
match {
header Content-Type text/css*
header Content-Type text/csv*
header Content-Type text/html*
header Content-Type text/javascript*
header Content-Type text/markdown*
header Content-Type text/plain*
header Content-Type text/xml*
header Content-Type application/json*
header Content-Type application/javascript*
header Content-Type application/xml*
header Content-Type application/rss+xml*
header Content-Type image/svg+xml*
}
}
EOF
)
if printf '%s\n' "$normalized_config" | grep -Eiq 'cache-control.*immutable'; then
echo "Caddyfile must not force immutable caching; the backend owns asset cache policy" >&2
exit 1
fi
if ! printf '%s\n' "$normalized_config" | grep -Eq '^reverse_proxy localhost:8080([[:space:]]|$)'; then
echo "Caddyfile must continue proxying all application routes to localhost:8080" >&2
exit 1
fi
if printf '%s\n' "$normalized_config" | grep -Eq '^import([[:space:]]|$)'; then
echo "Caddyfile must not import configuration outside this canonical policy check" >&2
exit 1
fi
if printf '%s\n' "$normalized_config" | grep -Eiq '^flush_interval([[:space:]]|$)'; then
echo "Caddyfile must leave flush_interval unset so SSE auto-flushing and client cancellation remain intact" >&2
exit 1
fi
encode_directive_count=$(printf '%s\n' "$normalized_config" | awk '$1 == "encode" { count++ } END { print count + 0 }')
if [ "$encode_directive_count" -ne 1 ]; then
echo "Caddyfile must contain exactly one explicit encode block" >&2
exit 1
fi
actual_encode_block=$(printf '%s\n' "$normalized_config" | awk '
$1 == "encode" { in_block = 1 }
in_block {
print
for (field = 1; field <= NF; field++) {
if ($field == "{") depth++
if ($field == "}") depth--
}
if (depth == 0) exit
}
')
if [ "$actual_encode_block" != "$expected_encode_block" ]; then
echo "Caddyfile encode block must keep the canonical non-SSE compression policy" >&2
exit 1
fi
echo "Caddyfile preserves backend cache policy, SSE streaming, and non-SSE compression"
+78
View File
@@ -0,0 +1,78 @@
#!/bin/bash
set -euo pipefail
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEPLOY_DIR="$(cd "${TEST_DIR}/.." && pwd)"
SCRIPT="${DEPLOY_DIR}/apple-container.sh"
TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/sub2api-apple-test.XXXXXX")"
STATE_DIR="${TEST_ROOT}/state"
ENV_FILE="${TEST_ROOT}/sub2api.env"
cleanup() {
rm -rf "${TEST_ROOT}"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$*" >&2
exit 1
}
assert_exists() {
[[ -e "$1" ]] || fail "Expected path to exist: $1"
}
assert_missing() {
[[ ! -e "$1" ]] || fail "Expected path to be absent: $1"
}
export FAKE_CONTAINER_STATE="${STATE_DIR}"
export PATH="${TEST_DIR}/fixtures/bin:${PATH}"
export SUB2API_ENV_FILE="${ENV_FILE}"
mkdir -p "${STATE_DIR}"
"${SCRIPT}" init
[[ "$(stat -f '%Lp' "${ENV_FILE}")" == "600" ]] || fail "init did not create a mode-600 env file"
grep -q '^POSTGRES_PASSWORD=change_this_secure_password$' "${ENV_FILE}" && fail "init retained the placeholder password"
chmod 644 "${ENV_FILE}"
if "${SCRIPT}" up >/dev/null 2>&1; then
fail "up accepted an insecure env file"
fi
chmod 600 "${ENV_FILE}"
"${SCRIPT}" up
assert_exists "${STATE_DIR}/containers/sub2api-apple"
assert_exists "${STATE_DIR}/containers/sub2api-apple-postgres"
assert_exists "${STATE_DIR}/containers/sub2api-apple-redis"
assert_exists "${STATE_DIR}/running/sub2api-apple"
"${SCRIPT}" status >/dev/null
"${SCRIPT}" up --recreate
assert_exists "${STATE_DIR}/running/sub2api-apple"
"${SCRIPT}" down
assert_missing "${STATE_DIR}/running/sub2api-apple"
assert_missing "${STATE_DIR}/running/sub2api-apple-postgres"
assert_missing "${STATE_DIR}/running/sub2api-apple-redis"
"${SCRIPT}" destroy --yes
assert_missing "${STATE_DIR}/containers/sub2api-apple"
assert_missing "${STATE_DIR}/networks/sub2api-apple"
assert_exists "${STATE_DIR}/volumes/sub2api-apple-data"
"${SCRIPT}" up
"${SCRIPT}" destroy --volumes --yes
assert_missing "${STATE_DIR}/volumes/sub2api-apple-data"
assert_missing "${STATE_DIR}/volumes/sub2api-apple-postgres-data"
assert_missing "${STATE_DIR}/volumes/sub2api-apple-redis-data"
touch "${STATE_DIR}/system-running"
touch "${STATE_DIR}/containers/sub2api-apple"
touch "${STATE_DIR}/unowned/container/sub2api-apple"
if "${SCRIPT}" status >/dev/null 2>&1; then
fail "status accepted an unowned same-name container"
fi
printf 'Apple container lifecycle tests passed.\n'
+44
View File
@@ -0,0 +1,44 @@
#!/bin/sh
set -eu
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$repo_root"
check_application_security_opt() {
file=$1
count=$(
awk '
$0 == " sub2api:" {
in_application = 1
next
}
in_application && $0 ~ /^ [A-Za-z0-9_-]+:$/ {
in_application = 0
}
in_application && $0 == " security_opt:" {
in_security_opt = 1
next
}
in_application && in_security_opt && $0 == " - no-new-privileges:true" {
count++
}
END { print count + 0 }
' "$file"
)
if [ "$count" -ne 1 ]; then
printf '%s must enable no-new-privileges exactly once for the sub2api service\n' "$file" >&2
exit 1
fi
}
for compose_file in \
deploy/docker-compose.yml \
deploy/docker-compose.local.yml \
deploy/docker-compose.standalone.yml \
deploy/docker-compose.dev.yml
do
check_application_security_opt "$compose_file"
done
printf 'docker compose security test passed\n'
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
set -eu
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$repo_root"
fail() {
printf 'docker runtime resources test failed: %s\n' "$1" >&2
exit 1
}
assert_line() {
file=$1
line=$2
grep -Fqx "$line" "$file" || fail "$file is missing: $line"
}
assert_count() {
file=$1
line=$2
expected=$3
actual=$(grep -Fxc "$line" "$file" || true)
[ "$actual" -eq "$expected" ] || fail "$file has $actual occurrences of '$line', expected $expected"
}
test -s backend/resources/model-pricing/model_prices_and_context_window.json || \
fail 'fallback pricing data is missing or empty'
assert_line Dockerfile.goreleaser 'COPY --chown=sub2api:sub2api backend/resources /app/resources'
assert_line deploy/Dockerfile 'COPY --from=backend-builder --chown=sub2api:sub2api /app/backend/resources /app/resources'
assert_count .goreleaser.yaml ' - backend/resources' 4
assert_count .goreleaser.simple.yaml ' - backend/resources' 1
printf 'docker runtime resources test passed\n'
+164
View File
@@ -0,0 +1,164 @@
#!/bin/bash
set -eu
STATE_DIR="${FAKE_CONTAINER_STATE:?FAKE_CONTAINER_STATE is required}"
mkdir -p \
"${STATE_DIR}/containers" \
"${STATE_DIR}/running" \
"${STATE_DIR}/networks" \
"${STATE_DIR}/volumes" \
"${STATE_DIR}/unowned/container" \
"${STATE_DIR}/unowned/network" \
"${STATE_DIR}/unowned/volume"
list_names() {
local directory=$1
local path
for path in "${directory}"/*; do
[[ -e "${path}" ]] || continue
basename "${path}"
done
}
last_argument() {
local value=""
for value in "$@"; do :; done
printf '%s\n' "${value}"
}
inspect_resource() {
local resource_type=$1
local resource_name=$2
local label_value="apple-container"
local address="192.168.65.4/24"
if [[ -e "${STATE_DIR}/unowned/${resource_type}/${resource_name}" ]]; then
label_value="other"
fi
case "${resource_name}" in
sub2api-apple-postgres) address="192.168.65.2/24" ;;
sub2api-apple-redis) address="192.168.65.3/24" ;;
esac
printf '[{"configuration":{"labels":{"org.sub2api.stack":"%s"}},"status":{"networks":[{"ipv4Address":"%s"}]}}]\n' \
"${label_value}" "${address}"
}
command=${1-}
if [[ $# -gt 0 ]]; then shift; fi
case "${command}" in
--version)
echo "container CLI version 1.1.0 (build: release, commit: fake)"
;;
system)
subcommand=${1-}
case "${subcommand}" in
status) [[ -e "${STATE_DIR}/system-running" ]] ;;
start) touch "${STATE_DIR}/system-running" ;;
stop) rm -f "${STATE_DIR}/system-running" "${STATE_DIR}/running"/* ;;
*) exit 1 ;;
esac
;;
list)
include_all=false
for argument in "$@"; do
[[ "${argument}" == "--all" || "${argument}" == "-a" ]] && include_all=true
done
if [[ "${include_all}" == true ]]; then
list_names "${STATE_DIR}/containers"
else
list_names "${STATE_DIR}/running"
fi
;;
network)
subcommand=${1-}
shift || true
case "${subcommand}" in
list)
echo default
list_names "${STATE_DIR}/networks"
;;
create) touch "${STATE_DIR}/networks/$(last_argument "$@")" ;;
inspect) inspect_resource network "${1}" ;;
delete) rm -f "${STATE_DIR}/networks/${1}" ;;
*) exit 1 ;;
esac
;;
volume)
subcommand=${1-}
shift || true
case "${subcommand}" in
list) list_names "${STATE_DIR}/volumes" ;;
create) touch "${STATE_DIR}/volumes/$(last_argument "$@")" ;;
inspect) inspect_resource volume "${1}" ;;
delete) rm -f "${STATE_DIR}/volumes/${1}" ;;
*) exit 1 ;;
esac
;;
image)
subcommand=${1-}
case "${subcommand}" in
inspect|pull) exit 0 ;;
*) exit 1 ;;
esac
;;
create)
name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name)
name=$2
shift 2
;;
--label|--network|--platform|--ulimit|--env-file|--volume|--entrypoint|--publish)
shift 2
;;
*)
shift
;;
esac
done
[[ -n "${name}" ]]
touch "${STATE_DIR}/containers/${name}"
;;
inspect)
inspect_resource container "${1}"
;;
start)
touch "${STATE_DIR}/running/${1}"
;;
stop)
for argument in "$@"; do
case "${argument}" in
--time|--signal) skip_next=true ;;
[0-9]*|SIG*) ;;
*) rm -f "${STATE_DIR}/running/${argument}" ;;
esac
done
;;
delete)
for argument in "$@"; do
case "${argument}" in
--force|-f) ;;
*)
rm -f "${STATE_DIR}/running/${argument}"
rm -f "${STATE_DIR}/containers/${argument}"
;;
esac
done
;;
exec)
echo 1
;;
logs|copy)
exit 0
;;
*)
echo "Unsupported fake container command: ${command} $*" >&2
exit 1
;;
esac
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
set -eu
printf '{"status":"ok"}\n'
+103
View File
@@ -0,0 +1,103 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
cat > "$TEMP_DIR/curl" <<'EOF'
#!/bin/bash
printf '%s\n' "$@" > "$CURL_ARGS_LOG"
env > "${CURL_ARGS_LOG}.env"
cat > "${CURL_ARGS_LOG}.stdin"
EOF
chmod +x "$TEMP_DIR/curl"
mkdir "$TEMP_DIR/home"
cat > "$TEMP_DIR/home/.curlrc" <<'EOF'
url = "https://example.com/collect"
header = "X-Leaked-From-Curlrc: yes"
EOF
run_api_curl() {
CURL_ARGS_LOG="$1" HOME="$TEMP_DIR/home" PATH="$TEMP_DIR:$PATH" UPDATE_GITHUB_TOKEN="${2:-}" \
GITHUB_TOKEN="github-fallback" GH_TOKEN="gh-fallback" \
bash -c 'source <(head -n -1 "$1"); github_api_curl -s "$2"' bash \
"$ROOT_DIR/deploy/install.sh" "https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest"
}
run_api_curl "$TEMP_DIR/authenticated" "update-secret"
test "$(head -n 1 "$TEMP_DIR/authenticated")" = '-q'
grep -Fxq -- '--config' "$TEMP_DIR/authenticated"
grep -Fxq -- '-' "$TEMP_DIR/authenticated"
grep -Fxq -- '--globoff' "$TEMP_DIR/authenticated"
grep -Fxq 'header = "Authorization: Bearer update-secret"' "$TEMP_DIR/authenticated.stdin"
if grep -Fq 'update-secret' "$TEMP_DIR/authenticated"; then
echo "installer exposed the update token in curl argv" >&2
exit 1
fi
if grep -Eq 'update-secret|github-fallback|gh-fallback' "$TEMP_DIR/authenticated.env"; then
echo "installer exposed a token in curl environment" >&2
exit 1
fi
test "$(grep -Fxc 'https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest' "$TEMP_DIR/authenticated")" -eq 1
if grep -Fq 'example.com/collect' "$TEMP_DIR/authenticated" || grep -Fq 'X-Leaked-From-Curlrc' "$TEMP_DIR/authenticated" ||
grep -Fq 'example.com/collect' "$TEMP_DIR/authenticated.stdin" || grep -Fq 'X-Leaked-From-Curlrc' "$TEMP_DIR/authenticated.stdin"; then
echo "installer allowed hostile curl config into authenticated invocation" >&2
exit 1
fi
run_api_curl "$TEMP_DIR/anonymous"
test "$(head -n 1 "$TEMP_DIR/anonymous")" = '-q'
if grep -Eq 'github-fallback|gh-fallback' "$TEMP_DIR/anonymous.env"; then
echo "installer exposed a fallback token in anonymous curl environment" >&2
exit 1
fi
if grep -Fq 'Authorization:' "$TEMP_DIR/anonymous"; then
echo "installer unexpectedly used a fallback token" >&2
exit 1
fi
test ! -s "$TEMP_DIR/anonymous.stdin"
test "$(grep -Fxc 'https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest' "$TEMP_DIR/anonymous")" -eq 1
if grep -Fq 'example.com/collect' "$TEMP_DIR/anonymous" || grep -Fq 'X-Leaked-From-Curlrc' "$TEMP_DIR/anonymous"; then
echo "installer allowed hostile curl config into anonymous invocation" >&2
exit 1
fi
assert_unsafe_invocation_rejected() {
local name=$1
shift
rm -f "$TEMP_DIR/$name" "$TEMP_DIR/$name.stdin"
if CURL_ARGS_LOG="$TEMP_DIR/$name" PATH="$TEMP_DIR:$PATH" UPDATE_GITHUB_TOKEN="update-secret" \
bash -c 'source <(head -n -1 "$1"); shift; github_api_curl "$@"' bash \
"$ROOT_DIR/deploy/install.sh" "$@" 2>/dev/null; then
echo "installer accepted unsafe curl invocation: $name" >&2
exit 1
fi
if [ -e "$TEMP_DIR/$name" ]; then
echo "installer invoked curl for unsafe request: $name" >&2
exit 1
fi
}
assert_unsafe_invocation_rejected non-api -s \
"https://github.com/Wei-Shaw/sub2api/releases/download/v1/asset"
assert_unsafe_invocation_rejected mixed-host -s \
"https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest" \
"https://example.com/collect"
assert_unsafe_invocation_rejected multiple-api -s \
"https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest" \
"https://api.github.com/repos/Wei-Shaw/sub2api/releases"
assert_unsafe_invocation_rejected url-option -s --url \
"https://example.com/collect" \
"https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest"
# Every installer release API request must use the scoped helper.
test "$(grep -c 'github_api_curl .*https://api.github.com/' "$ROOT_DIR/deploy/install.sh")" -eq 3
# Asset and checksum downloads must continue to call curl directly.
grep -Fq 'curl -sL "$download_url"' "$ROOT_DIR/deploy/install.sh"
grep -Fq 'curl -sL "$checksum_url"' "$ROOT_DIR/deploy/install.sh"
echo "install GitHub token checks passed"