1. 为什么要把 NewAPI 和 Sub2API 放在一起部署
如果你手上有多个模型渠道,又想让团队或自己的几个客户端统一走一个入口,NewAPI 负责的是「统一网关 + 令牌分发 + 用量统计」,Sub2API 负责的是「订阅式额度管理 + 用户套餐」,两者拼起来才是一套完整的自用中转站。单独跑 NewAPI,你能拿到一个 OpenAI 兼容的/v1/chat/completions端点;单独跑 Sub2API,你能做用户和套餐,但缺少上游渠道聚合。合在一起,前端用户拿 Sub2API 发的订阅 Key,后端实际请求打到 NewAPI 聚合的渠道上,链路才闭环。
这篇教程面向的是需要统一管理多模型 API 通道的开发者,交付物很具体:一份可复制的docker-compose.yml、一份.env.secret骨架、TaoToken 统一 Key 的接入步骤,以及部署完成后的连通性验证命令。整套环境我在 Ubuntu 24.04 上跑通过,内存 2G 的机器也能起来,关键是几个参数必须显式限制,否则 MySQL 和 Postgres 会互相抢内存。
先说清楚整体拓扑,不然后面配置容易迷路。NewAPI 自带 MySQL 和 Redis,Sub2API 单独用 PostgreSQL,但复用 NewAPI 的 Redis(用 DB1 区分)。两个应用都只监听本地回环,公网暴露交给 cloudflared 隧道。这样做的原因是:数据库端口一个都不对外,攻击面小;隧道重启会换临时域名,所以要把域名写回 NewAPI 的ServerAddress,令牌详情页才能生成完整调用链接。
TaoToken 在这里的角色是上游统一 Key 提供方。你在 NewAPI 里新建渠道时,把 TaoToken 的 API 地址和 Key 填进去,就能把它的模型能力挂到自己的网关下,再由 NewAPI 分发给 Sub2API 的用户。下面从环境准备开始,一步步来。
2. 环境准备与 TaoToken 前置配置
2.1 系统确认与基础工具
先确认系统版本,命令输出里能看到VERSION_CODENAME就行:
cat /etc/os-release | head -5装基础工具包:
sudo apt-get update -y sudo apt-get install -y ca-certificates curl gnupg lsb-release vim2.2 安装 Docker 与 Docker Compose
加 Docker 官方 GPG 密钥,国内网络优先走清华镜像:
sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL --connect-timeout 8 https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu/gpg \ -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc写 apt 源,先探测清华镜像是否可达,不通再回退官方:
CODENAME=$(. /etc/os-release && echo "$VERSION_CODENAME") ARCH=$(dpkg --print-architecture) REPO=https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu curl -fsSI --connect-timeout 5 "$REPO/dists/$CODENAME/Release" >/dev/null \ || REPO=https://download.docker.com/linux/ubuntu echo "deb [arch=$ARCH signed-by=/etc/apt/keyrings/docker.asc] $REPO $CODENAME stable" \ | sudo tee /etc/apt/sources.list.d/docker.list sudo apt-get update -y安装 Docker 组件:
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin sudo systemctl enable --now docker sudo usermod -aG docker $USER验证:
docker --version docker compose version注意:
usermod -aG docker加组之后要重新登录 SSH 才不用 sudo 跑 docker。本教程后面命令仍带sudo兼容。
配置镜像加速,同时限制容器日志大小,避免长跑把磁盘写满:
sudo tee /etc/docker/daemon.json >/dev/null <<'EOF' { "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com", "https://docker.1panel.live" ], "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } } EOF sudo systemctl restart docker docker info | grep -A3 'Registry Mirrors'log-opts限制单容器日志最多 30 MB(10×3),这个在小盘 VPS 上很关键。
2.3 获取 TaoToken 统一 Key
打开 TaoToken 控制台,在 API Keys 页面创建一个新 Key,复制保存。这个 Key 就是后面填进 NewAPI 渠道的凭证。同时记下 API 地址https://taotoken.net/api,NewAPI 新建渠道时「代理地址」填这个,「密钥」填刚复制的 Key。
如果你还没注册,先走官网入口完成账号创建,再进控制台建 Key。整个流程不需要额外配置,建完 Key 就能直接用。
3. 部署 NewAPI 并接入 TaoToken 渠道
3.1 创建目录与生成密码
sudo mkdir -p /opt/new-api/{data,logs,mysql,redis} sudo chown -R $USER:$USER /opt/new-api cd /opt/new-api一次性生成 4 个随机密码写入.env.secret,只生成一次,丢了就要全部重置:
cat > /opt/new-api/.env.secret <<EOF MYSQL_ROOT_PASSWORD=$(openssl rand -base64 24 | tr -d '=+/' | cut -c1-24) MYSQL_USER=newapi MYSQL_PASSWORD=$(openssl rand -base64 24 | tr -d '=+/' | cut -c1-24) SESSION_SECRET=$(openssl rand -hex 32) EOF chmod 600 /opt/new-api/.env.secret cat /opt/new-api/.env.secret把输出的 4 行抄到密码管理器。
3.2 编写 docker-compose.yml
set -a; source /opt/new-api/.env.secret; set +a cat > /opt/new-api/docker-compose.yml <<EOF services: new-api: image: calciumion/new-api:latest container_name: new-api restart: unless-stopped depends_on: mysql: condition: service_healthy redis: condition: service_started ports: - "3000:3000" environment: TZ: Asia/Shanghai SQL_DSN: "newapi:${MYSQL_PASSWORD}@tcp(mysql:3306)/new-api?charset=utf8mb4&parseTime=True&loc=Local" REDIS_CONN_STRING: "redis://redis:6379/0" SESSION_SECRET: "${SESSION_SECRET}" CRYPTO_SECRET: "${SESSION_SECRET}" SYSTEM_NAME: "WeskyApi" GIN_MODE: release ERROR_LOG_ENABLED: "true" volumes: - ./data:/data - ./logs:/app/logs healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/api/status >/dev/null || exit 1"] interval: 30s timeout: 5s retries: 10 mysql: image: mysql:8.4 container_name: new-api-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}" MYSQL_DATABASE: "new-api" MYSQL_USER: "newapi" MYSQL_PASSWORD: "${MYSQL_PASSWORD}" TZ: Asia/Shanghai command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --innodb-buffer-pool-size=128M - --max-connections=200 - --performance-schema=OFF volumes: - ./mysql:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"] interval: 10s timeout: 5s retries: 20 redis: image: redis:7-alpine container_name: new-api-redis restart: unless-stopped command: ["redis-server", "--maxmemory", "128mb", "--maxmemory-policy", "allkeys-lru", "--save", "900", "1"] volumes: - ./redis:/data EOF关键调优(小内存机器必须):MySQL 的
--innodb-buffer-pool-size=128M限制缓冲池,默认会自适应到 25% 内存;--performance-schema=OFF省约 50MB;Redis 的--maxmemory 128mb加 LRU 策略防止无限增长。
3.3 启动并初始化管理员
cd /opt/new-api sudo docker compose pull sudo docker compose up -d sudo docker compose ps等服务就绪:
for i in $(seq 1 36); do if curl -fsS http://127.0.0.1:3000/api/status >/dev/null; then echo "ok"; break; fi echo "等待中 $i/36"; sleep 5 done当前版本 NewAPI 不再预置默认账户,必须主动调/api/setup接口创建 root:
ROOT_USER=root ROOT_PASSWORD="Wesky-$(openssl rand -base64 16 | tr -d '=+/' | cut -c1-16)" echo "ROOT_USERNAME=$ROOT_USER" | sudo tee -a /opt/new-api/.env.secret echo "ROOT_PASSWORD=$ROOT_PASSWORD" | sudo tee -a /opt/new-api/.env.secret echo ">>> 务必记下: $ROOT_USER / $ROOT_PASSWORD" curl -sS -X POST http://127.0.0.1:3000/api/setup \ -H 'Content-Type: application/json' \ -d "{\"Username\":\"$ROOT_USER\",\"Password\":\"$ROOT_PASSWORD\",\"ConfirmPassword\":\"$ROOT_PASSWORD\",\"SelfUseModeEnabled\":false,\"DemoSiteEnabled\":false}" echo期望返回{"message":"系统初始化成功","success":true}。
登录拿 cookie 和 uid:
LOGIN_RESP=$(curl -sS -c /tmp/newapi_cookie.txt -X POST http://127.0.0.1:3000/api/user/login \ -H 'Content-Type: application/json' \ -d "{\"username\":\"$ROOT_USER\",\"password\":\"$ROOT_PASSWORD\"}") echo "$LOGIN_RESP" UID=$(echo "$LOGIN_RESP" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["id"])') echo "UID=$UID"关键坑:调任何管理员接口都要带
New-Api-User: $UID这个 HTTP 头,仅 cookie 不够。否则会得到Unauthorized, New-Api-User header not provided。
3.4 在 NewAPI 里新建 TaoToken 渠道
登录 NewAPI 后台,进入「渠道」→「添加渠道」。关键字段这样填:
| 字段 | 值 |
|---|---|
| 类型 | OpenAI |
| 名称 | TaoToken |
| 代理地址 | https://taotoken.net/api |
| 密钥 | 你在 TaoToken 控制台创建的 Key |
| 模型 | 按需勾选,或填gpt-4o,gpt-4o-mini,claude-3-5-sonnet等 |
保存后点「测试」按钮,返回绿色即渠道可用。这一步做完,NewAPI 就已经能通过 TaoToken 转发请求了。
4. 部署 Sub2API 并复用 NewAPI 的 Redis
4.1 创建目录与生成机密
sudo mkdir -p /opt/sub2api/{data,postgres} sudo chown -R $USER:$USER /opt/sub2api cd /opt/sub2api cat > /opt/sub2api/.env.secret <<EOF POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '=+/' | cut -c1-24) JWT_SECRET=$(openssl rand -hex 32) TOTP_ENCRYPTION_KEY=$(openssl rand -hex 32) ADMIN_EMAIL=admin@weskyapi.local ADMIN_PASSWORD=Wesky-$(openssl rand -base64 16 | tr -d '=+/' | cut -c1-16) EOF chmod 600 /opt/sub2api/.env.secret cat /opt/sub2api/.env.secret
JWT_SECRET必须固定,变了所有用户被踢下线;TOTP_ENCRYPTION_KEY必须固定,变了所有 2FA 失效;ADMIN_EMAIL是登录用户名。
4.2 编写 docker-compose.yml
set -a; source /opt/sub2api/.env.secret; set +a cat > /opt/sub2api/docker-compose.yml <<EOF services: sub2api: image: weishaw/sub2api:latest container_name: sub2api restart: unless-stopped ulimits: nofile: { soft: 65535, hard: 65535 } ports: - "127.0.0.1:8080:8080" depends_on: postgres: condition: service_healthy volumes: - ./data:/app/data environment: AUTO_SETUP: "true" SERVER_HOST: "0.0.0.0" SERVER_PORT: "8080" SERVER_MODE: "release" RUN_MODE: "standard" DATABASE_HOST: postgres DATABASE_PORT: "5432" DATABASE_USER: sub2api DATABASE_PASSWORD: "${POSTGRES_PASSWORD}" DATABASE_DBNAME: sub2api DATABASE_SSLMODE: disable DATABASE_MAX_OPEN_CONNS: "30" DATABASE_MAX_IDLE_CONNS: "5" REDIS_HOST: new-api-redis REDIS_PORT: "6379" REDIS_DB: "1" REDIS_POOL_SIZE: "128" REDIS_MIN_IDLE_CONNS: "4" JWT_SECRET: "${JWT_SECRET}" JWT_EXPIRE_HOUR: "168" TOTP_ENCRYPTION_KEY: "${TOTP_ENCRYPTION_KEY}" ADMIN_EMAIL: "${ADMIN_EMAIL}" ADMIN_PASSWORD: "${ADMIN_PASSWORD}" TZ: Asia/Shanghai SECURITY_URL_ALLOWLIST_ENABLED: "false" SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP: "true" SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS: "true" networks: - sub2api-network - newapi-shared healthcheck: test: ["CMD", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 5 start_period: 60s postgres: image: postgres:18-alpine container_name: sub2api-postgres restart: unless-stopped shm_size: 128mb environment: PGDATA: /var/lib/postgresql/data POSTGRES_USER: sub2api POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" POSTGRES_DB: sub2api TZ: Asia/Shanghai command: - postgres - -c - shared_buffers=64MB - -c - effective_cache_size=192MB - -c - max_connections=80 volumes: - ./postgres:/var/lib/postgresql/data networks: - sub2api-network healthcheck: test: ["CMD-SHELL", "pg_isready -U sub2api -d sub2api"] interval: 10s timeout: 5s retries: 10 start_period: 30s networks: sub2api-network: driver: bridge newapi-shared: external: true name: new-api_default EOF三个不能省的细节:
PGDATA: /var/lib/postgresql/data必须显式设置。postgres:18-alpine默认PGDATA=/var/lib/postgresql/18/docker,不显式设的话挂载到./postgres的卷里不会落数据,重启就 initdb,账号订阅全丢。shm_size: 128mb:PG18 默认 64MB 太小,复杂查询会报共享内存不足。networks.newapi-shared.external: true name: new-api_default:把 sub2api 接入 new-api 的网络,才能用容器名new-api-redis解析到那个 redis 容器。
4.3 启动并验证
cd /opt/sub2api sudo docker compose pull sudo docker compose up -d sudo docker compose ps期望两个容器都Up (healthy),且sub2api的 PORTS 列必须是127.0.0.1:8080,不是0.0.0.0。
for i in $(seq 1 36); do if curl -fsS http://127.0.0.1:8080/health >/dev/null; then echo ok; break; fi echo "等待中 $i"; sleep 5 done curl -sS http://127.0.0.1:8080/health期望返回{"status":"ok"}。
5. 公网暴露与调用验证
5.1 安装 cloudflared 并起两个隧道
sudo mkdir -p --mode=0755 /usr/share/keyrings curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \ | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' \ | sudo tee /etc/apt/sources.list.d/cloudflared.list sudo apt-get update -y sudo apt-get install -y cloudflared cloudflared --version写 NewAPI 的 systemd 单元:
sudo tee /etc/systemd/system/cloudflared-newapi.service >/dev/null <<'EOF' [Unit] Description=cloudflared quick tunnel for new-api After=network-online.target docker.service Wants=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/cloudflared tunnel --url http://127.0.0.1:3000 --no-autoupdate --metrics 127.0.0.1:20241 Restart=on-failure RestartSec=5 User=root StandardOutput=append:/var/log/cloudflared-newapi.log StandardError=append:/var/log/cloudflared-newapi.log ExecStartPre=/bin/sh -c 'command -v cloudflared | xargs -I{} ln -sf {} /usr/local/bin/cloudflared' [Install] WantedBy=multi-user.target EOF sudo touch /var/log/cloudflared-newapi.log sudo chmod 640 /var/log/cloudflared-newapi.log sudo systemctl daemon-reload sudo systemctl enable --now cloudflared-newapi.serviceSub2API 的单元,三处差异:--url指向 8080、--metrics用 20242、日志路径不同:
sudo tee /etc/systemd/system/cloudflared-sub2api.service >/dev/null <<'EOF' [Unit] Description=cloudflared quick tunnel for Sub2API After=network-online.target docker.service Wants=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/cloudflared tunnel --url http://127.0.0.1:8080 --no-autoupdate --metrics 127.0.0.1:20242 Restart=on-failure RestartSec=5 User=root StandardOutput=append:/var/log/cloudflared-sub2api.log StandardError=append:/var/log/cloudflared-sub2api.log ExecStartPre=/bin/sh -c 'command -v cloudflared | xargs -I{} ln -sf {} /usr/local/bin/cloudflared' [Install] WantedBy=multi-user.target EOF sudo touch /var/log/cloudflared-sub2api.log sudo chmod 640 /var/log/cloudflared-sub2api.log sudo systemctl daemon-reload sudo systemctl enable --now cloudflared-sub2api.service拿两个临时域名:
for i in $(seq 1 30); do URL=$(sudo grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' /var/log/cloudflared-newapi.log | head -1) [ -n "$URL" ] && { echo "NewAPI URL: $URL"; break; } sleep 3 done echo "$URL" | sudo tee /opt/new-api/tunnel_url.txt for i in $(seq 1 30); do URL2=$(sudo grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' /var/log/cloudflared-sub2api.log | head -1) [ -n "$URL2" ] && { echo "Sub2API URL: $URL2"; break; } sleep 3 done echo "$URL2" | sudo tee /opt/sub2api/tunnel_url.txt把 NewAPI 的公网地址写回ServerAddress,令牌详情页才能生成完整链接:
curl -sS -b /tmp/newapi_cookie.txt -X PUT http://127.0.0.1:3000/api/option/ \ -H 'Content-Type: application/json' -H "New-Api-User: $UID" \ -d "{\"key\":\"ServerAddress\",\"value\":\"$URL\"}" echo5.2 从外部验证连通性
从你自己的电脑(不是服务器)执行:
curl -sS https://你的tunnel域名.trycloudflare.com/api/status | head -c 200 curl -sS https://你的sub2api域名.trycloudflare.com/health期望分别返回 NewAPI 状态 JSON 和{"status":"ok"}。
5.3 用 TaoToken Key 跑一次真实请求
在 NewAPI 后台「令牌」页面创建一个令牌,复制sk-开头的 Key。然后从外部发起一次对话请求:
curl -sS https://你的tunnel域名.trycloudflare.com/v1/chat/completions \ -H "Authorization: Bearer sk-你的NewAPI令牌" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "用一句话说明什么是统一 API 网关"}], "max_tokens": 100 }'返回里能看到choices[0].message.content就说明整条链路通了:客户端 → NewAPI → TaoToken → 模型 → 原路返回。这一步是整个部署的验收动作,跑通它比看任何日志都直观。
6. 常见报错排查
| 症状 | 可能原因 | 处置 |
|---|---|---|
docker compose pull卡死 | 镜像源不可达 | 加加速器后sudo systemctl restart docker |
| MySQL OOM 自动重启 | buffer-pool 没限制 | 确认 compose 里有--innodb-buffer-pool-size=128M,加 swap |
Unauthorized, New-Api-User header not provided | 调管理员接口忘带 header | 所有/api/option、/api/user/...必须带-H "New-Api-User: $UID" |
| 第二次跑 setup 报「用户名/密码不正确」 | DB 已有 root,密码不对 | 用.env.secret里的 ROOT_PASSWORD,不要重新生成 |
Sub2API 启动报POSTGRES_PASSWORD is required | env 没带过去 | 检查set -a; source .env.secret; set +a后再执行 compose |
| Sub2API 重启数据全丢 | PGDATA没显式设 | 见 4.2 节关键细节 1 |
| Sub2API 连不上 redis | external 网络名错 | docker network ls看 new-api 的网络名是不是new-api_default |
| Tunnel URL 突然不通 | cloudflared 重启换了 URL | cat /opt/*/tunnel_url.txt拿新地址,或看日志 |
| 服务器自己 curl trycloudflare 失败 | 国内 DNS 屏蔽 | 不影响外部,从本机/手机访问验证 |
| 两个 tunnel 同时只起来一个 | metrics 端口撞了 | 第二个改成 20242 |
排查时优先看容器日志,sudo docker logs --tail 100 new-api和sudo docker logs --tail 100 sub2api能覆盖八成问题。隧道问题看sudo journalctl -u cloudflared-newapi.service -n 100 --no-pager。
7. 后续接入与运维建议
部署完成后,日常运维主要盯三件事:容器健康状态、隧道 URL 变化、磁盘占用。sudo docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'一眼能看完所有容器;free -h和df -h各看一眼内存和磁盘。
如果你打算长期跑编码类或 Agent 类任务,建议把 NewAPI 的令牌换成 Coding Plan 对应的额度类型,这样用量统计更清晰,也方便按项目拆分。模型验证阶段可以直接用模型对话页面快速试通,不用每次都写 curl。
备份脚本建议接 cron,每天凌晨跑一次:
sudo mkdir -p /opt/backup sudo docker exec new-api-mysql sh -c \ 'MYSQL_PWD=$(grep ^MYSQL_PASSWORD /opt/new-api/.env.secret | cut -d= -f2) \ mysqldump -unewapi --single-transaction --quick --lock-tables=false new-api' \ | sudo tee /opt/backup/new-api-$(date +%F).sql >/dev/null sudo docker exec sub2api-postgres pg_dump -U sub2api sub2api \ | gzip | sudo tee /opt/backup/sub2api-$(date +%F).sql.gz >/dev/null安全清单里最容易被忽略的是 SSH:部署完把PasswordAuthentication no打开,只留密钥登录,同时用 ufw 或安全组只放 22/80/443,封掉 3000 和 8080。数据库端口一个都不要对外,这是底线。
最后提醒一点:trycloudflare.com的临时域名每次重启 cloudflared 都会变,只适合测试。要稳定用自有域名,走 Named Tunnel 把api.yourdomain.com和pincc.yourdomain.com分别指向 3000 和 8080,然后把ServerAddress同步更新成正式域名。这样令牌详情页生成的链接才不会隔天就失效。