这篇文章复盘一套已经实际运行过的 Sub2API 部署方案:在一台 Ubuntu 24.04 小内存 VPS 上运行 Sub2API 容器,把 PostgreSQL 和 Redis 放在外部托管服务中,再由 Caddy 提供域名和 HTTPS。
文章面向第一次接触服务器的人。所有 IP、域名和账号都是示例,必须替换成自己的值。不要把真实口令、API Key、SSH 私钥、数据库连接串或 .env 文件发到聊天、论坛和公开仓库。
本文以 Sub2API
v0.1.161的部署文件为核对基准,写于 2026 年 7 月。项目仍在更新,实际部署前应重新阅读 Sub2API 仓库 的 README 和发布说明。
先理解:我们究竟在搭建什么
Sub2API 是一个 API 网关。它接收客户端请求,根据后台配置选择上游账号或渠道,再把响应返回给客户端。它不是模型本身,也不会凭空提供模型额度。
本方案的数据流如下:
电脑上的 API 客户端
|
| HTTPS 443
v
api.example.com
|
v
Caddy 反向代理
|
| 仅服务器本机可访问
v
127.0.0.1:8080 -> Sub2API 容器
| |
| +-> 外部 Redis
+----------> 外部 PostgreSQL
|
+----------> 已获授权的上游 API
每一层的职责不同:
- DNS 把域名指向 VPS 公网 IP。
- Caddy 监听 80 和 443,自动申请 HTTPS 证书。
- Sub2API 只监听
127.0.0.1:8080,不直接暴露到互联网。 - PostgreSQL 保存账号、用户和业务数据。
- Redis 用于缓存、限流、队列等运行状态。
- 客户端只拿到 Sub2API 生成的下游 API Key,不应拿到上游账号凭据。
使用边界与风险
部署前先确认以下事项:
- 只添加自己拥有或明确获授权使用的上游账号。
- 阅读并遵守上游服务条款、账号地区要求和当地法律。
- 自建中转不会消除账号风控。数据中心 IP、多人共享、异常并发和跨地区登录都可能触发限制。
- Sub2API 项目 README 明确提示账号封禁、服务中断和数据损失风险,并声明未授权商业运营。
- 本文部署的是 API 服务,不是通用 VPN、住宅代理或翻墙服务。
个人或少量可信用户可以使用这套架构,但不要把注册入口和 API Key 随意公开。
方案选择与最低配置
本文采用的是低内存方案:
| 组件 | 部署位置 | 原因 |
|---|---|---|
| Sub2API | VPS 上的 Docker 容器 | 便于启动、更新和查看健康状态 |
| PostgreSQL | 外部托管服务 | 减少 VPS 内存占用 |
| Redis | 外部托管服务 | 减少 VPS 内存占用 |
| Caddy | VPS 宿主机 | 提供 HTTPS 和反向代理 |
| DNS | DuckDNS 或自己的域名服务商 | 将域名解析到 VPS |
实测 1 GB 内存、约 25 GB 磁盘并配有 2 GB 以上 Swap,可以承担一个人或少量人的轻量使用。但余量不大,不适合高并发、图片生成或大量账号调度。
如果想把 Sub2API、PostgreSQL 和 Redis 全部放在同一台机器,应改用上游的 docker-compose.local.yml,至少准备 2 GB 内存,稳妥起见使用 4 GB。不要在 1 GB 机器上无条件照搬完整三容器方案。
外部数据库和 Redis 的免费额度、休眠规则、连接数限制可能随时变化。免费不等于永久免费,也不等于有可靠性保证。
第一部分:创建 Ubuntu VPS
创建服务器时建议选择:
- 系统:Ubuntu 24.04 LTS。
- 架构:常见的 x86_64。
- 地区:选择客户端网络相对稳定、同时符合上游服务地区政策的机房。
- 登录:优先使用 SSH Ed25519 公钥,不依赖弱口令。
- 自动备份:预算允许时开启;否则至少自行备份数据库和配置。
美国机房通常能访问更多国际服务,但中国大陆到美国的延迟和丢包可能更高。VPS 提供的是数据中心 IP,不是住宅 IP。机房选择应同时考虑网络质量、上游条款和数据合规,不能只看延迟。
本文使用以下示例,全文保持一致:
VPS IP: 203.0.113.10
SSH 用户: linuxuser
API 域名: api.example.com
203.0.113.10 是文档专用地址,不能直接拿来连接。
第二部分:在 Windows 上准备 SSH
以下命令在 Windows PowerShell 中执行,不是在服务器中执行。
生成 SSH 密钥
ssh-keygen -t ed25519 -f "$env:USERPROFILE\.ssh\id_ed25519"
系统会询问是否设置私钥口令。个人电脑建议设置,并妥善保存。生成后有两个文件:
id_ed25519是私钥,绝对不能发给别人。id_ed25519.pub是公钥,可以添加到 VPS。
查看公钥:
Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub"
公钥通常以 ssh-ed25519 开头。复制完整的一行到服务器提供商的 SSH Key 页面。
连接服务器
ssh -i "$env:USERPROFILE\.ssh\id_ed25519" [email protected]
第一次连接会显示主机指纹。应先在 VPS 控制台核对指纹,再输入 yes。不能在没有核对的情况下盲目信任。
如果重装服务器后出现:
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!
先到提供商控制台确认新指纹确实属于刚重装的服务器,然后在 PowerShell 中删除旧记录:
ssh-keygen -R 203.0.113.10
再重新连接并核对新指纹。若没有重装或更换服务器,不要直接删除警告,它也可能意味着中间人攻击。
两个常见误解
运行 sudo 时,终端不会显示星号,也不会回显任何字符,这是正常的安全设计。输入完成后按回车即可。
查看 Ubuntu 版本的正确命令是:
cat /etc/os-release
路径中是连字符 -,不是波浪号 ~。
第三部分:初始化 Ubuntu
从这里开始,命令都在 SSH 登录后的 Ubuntu 终端中执行。
先确认身份和 sudo 权限:
whoami
sudo -v
sudo whoami
最后一条应输出 root。
更新系统和安装基础工具
sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl openssl ufw
如果升级安装了新内核,安排一次重启:
sudo reboot
等待几十秒后重新 SSH 登录。
检查内存、Swap 和磁盘
free -h
df -h /
swapon --show
1 GB VPS 最好有 2 GB 左右 Swap。Swap 不能替代内存,只是降低瞬时内存不足时被系统直接杀死进程的概率。
只有在 swapon --show 没有任何输出时,才创建 Swap 文件:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
完成后再次执行 free -h。不要重复向 /etc/fstab 添加同一行。
配置防火墙
先放行 SSH,再启用 UFW,否则可能把自己锁在服务器外面:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
最终只需要公网开放 22、80 和 443。不要放行 8080、5432 或 6379。
如果 VPS 提供商还有一层云防火墙,也要同步放行 22、80、443。UFW 和云防火墙任意一层阻断,外部都会无法访问。
第四部分:配置 DNS
可以使用自己的域名,也可以使用 DuckDNS 免费子域名。在 DNS 控制台中添加一条 A 记录,将 API 域名指向 VPS 的 IPv4 地址。
如果没有正确配置 IPv6,不要添加 AAAA 记录。错误或过期的 AAAA 记录会导致部分网络优先走 IPv6 后连接失败。
在 Windows PowerShell 中检查公共 DNS:
Resolve-DnsName api.example.com -Server 1.1.1.1
返回的 A 记录必须是自己的 VPS IP。DNS 生效前不要继续申请证书。
第五部分:安装 Docker 和 Caddy
Ubuntu 24.04 可以直接安装发行版维护的软件包:
sudo apt update
sudo apt install -y docker.io docker-compose-v2 caddy
sudo systemctl enable --now docker
sudo systemctl enable --now caddy
把当前管理用户加入 Docker 组:
sudo usermod -aG docker "$USER"
Docker 组基本等同于 root 权限,只能授予可信的服务器管理员。执行后退出 SSH 并重新登录,组权限才会完整生效。
验证安装:
docker --version
docker compose version
docker run --rm hello-world
sudo systemctl is-active caddy
hello-world 成功只证明 Docker 能拉取并运行容器,不代表 Sub2API 已部署。
第六部分:准备外部 PostgreSQL 和 Redis
docker-compose.standalone.yml 的含义是“只启动 Sub2API 应用”。它不会创建数据库和 Redis。如果没有外部服务,容器会不断重启或变成 unhealthy。
在托管服务控制台创建:
- PostgreSQL 15 或更高版本。
- Redis 7 或更高版本。
- 尽量选择靠近 VPS 的区域。
- 开启 TLS。
- 如果提供 IP 白名单,只允许 VPS 公网 IP。
PostgreSQL 连接地址通常类似:
postgresql://<用户>:<口令>@<主机>:<端口>/<数据库>?sslmode=require
Redis TLS 地址通常以 rediss:// 开头。Sub2API 的 standalone Compose 需要把连接串拆成独立字段,而不是把整条 URL 填到一个位置。
应提前记录这些字段:
.env 字段 | 内容 |
|---|---|
DATABASE_HOST | PostgreSQL 主机名 |
DATABASE_PORT | PostgreSQL 端口 |
DATABASE_USER | PostgreSQL 用户名 |
DATABASE_PASSWORD | PostgreSQL 口令 |
DATABASE_DBNAME | 数据库名称,注意不是 DATABASE_NAME |
DATABASE_SSLMODE | 托管服务通常使用 require |
REDIS_HOST | Redis 主机名 |
REDIS_PORT | Redis TLS 端口 |
REDIS_PASSWORD | Redis 口令 |
REDIS_ENABLE_TLS | 使用 TLS 时填 true |
免费数据库通常限制最大连接数。个人使用可从较小连接池开始,例如最大打开连接数 10、最大空闲连接数 5;Redis 连接池也不必使用面向高并发的巨大默认值。
第七部分:下载 Sub2API 部署文件
创建独立目录:
mkdir -p ~/sub2api
cd ~/sub2api
pwd
为了让教程可复现,这里固定读取 v0.1.161 的 Compose 文件:
curl -fsSL --retry 3 \
https://raw.githubusercontent.com/Wei-Shaw/sub2api/v0.1.161/deploy/docker-compose.standalone.yml \
-o docker-compose.yml
确认文件存在,并核对关键内容:
ls -lh docker-compose.yml
grep -E 'image:|AUTO_SETUP|BIND_HOST|DATABASE_HOST|REDIS_HOST' docker-compose.yml
该版本 Compose 中的镜像仍是 weishaw/sub2api:latest。这方便入门,但意味着未来重新拉取时可能得到新版本。更严格的生产环境应在测试后固定镜像 tag 或 digest,并保存回滚用的旧镜像信息。
不要从陌生网盘、聊天附件或二次打包站下载 Compose 文件。
第八部分:创建安全的 .env
先生成三个互不相同的随机值,分别用于管理员登录、JWT 签名和其他需要的本地秘密。每条命令的输出都只保存在自己的密码管理器中,不要截图或发送给别人:
openssl rand -hex 24
openssl rand -hex 32
openssl rand -hex 32
十六进制随机值没有 $、空格和 #,可以减少 Docker Compose 解析特殊字符时出错的概率。
创建文件:
umask 077
nano .env
先填写不含口令的部分:
BIND_HOST=127.0.0.1
SERVER_PORT=8080
SERVER_MODE=release
TZ=Asia/Shanghai
DATABASE_HOST=<数据库主机名>
DATABASE_PORT=<数据库端口>
DATABASE_USER=<数据库用户名>
DATABASE_DBNAME=<数据库名称>
DATABASE_SSLMODE=require
DATABASE_MAX_OPEN_CONNS=10
DATABASE_MAX_IDLE_CONNS=5
REDIS_HOST=<Redis主机名>
REDIS_PORT=<Redis端口>
REDIS_DB=0
REDIS_POOL_SIZE=32
REDIS_MIN_IDLE_CONNS=2
REDIS_ENABLE_TLS=true
ADMIN_EMAIL=<自己的管理员邮箱>
JWT_SECRET=<64位十六进制随机值>
JWT_EXPIRE_HOUR=24
然后按照同样的“字段名、ASCII 等号、值”格式,补齐以下三个敏感字段:
DATABASE_PASSWORD:数据库控制台提供的口令。REDIS_PASSWORD:Redis 控制台提供的口令。ADMIN_PASSWORD:刚生成的管理员随机口令。
如果托管服务提供的口令包含 $、# 或空格,应按照 Docker Compose 的 .env 规则使用单引号包住整个值。不要自行对独立字段再做 URL 百分号编码。
保存后收紧权限:
chmod 600 .env
ls -l .env
不要执行会把展开后配置完整打印到屏幕的命令并把输出发给别人。只做静默语法验证:
docker compose config --quiet
没有输出且退出码为 0,表示 Compose 能解析文件;它不代表数据库凭据一定正确。
为什么 JWT 必须固定
JWT_SECRET 用于签发登录会话。如果留空,应用可能在启动时生成随机值;容器重启后旧会话无法验证,网页就会提示“会话已过期,请重新登录”或登录后又回到登录页。
因此首次正式运行前就应设置固定、足够长的随机值。修改该值会让现有会话全部失效。
如果启用 TOTP 双因素认证,还要核对自己使用的 Compose 版本是否将 TOTP_ENCRYPTION_KEY 传入容器。v0.1.161 的完整 Compose 包含它,但 standalone 文件未包含该项,不能只在 .env 中添加字段就假设已经生效。
第九部分:启动 Sub2API
在 ~/sub2api 中执行:
docker compose up -d
docker compose ps
第一次启动需要连接数据库并执行迁移,健康状态可能暂时显示 starting。等待一会儿后检查:
docker inspect -f '{{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}} restart_count={{.RestartCount}}' sub2api
curl -i http://127.0.0.1:8080/health
正常结果应满足:
- 容器状态是
running healthy。 - 重启次数保持为 0。
- 健康接口返回 HTTP 200 和
{"status":"ok"}。 docker compose ps只显示一个 Sub2API 服务,这是 standalone 模式的正常现象。
确认端口只监听本机:
sudo ss -lntp | grep ':8080'
应该看到 127.0.0.1:8080,不能是 0.0.0.0:8080。
如果容器反复 unhealthy
先查看退出信息和最近日志:
docker inspect -f 'exit={{.State.ExitCode}} error={{.State.Error}} restart_count={{.RestartCount}}' sub2api
docker compose logs --tail=200 sub2api
重点检查:
DATABASE_DBNAME是否写成了错误的字段名。- PostgreSQL 是否要求 TLS,
DATABASE_SSLMODE是否匹配。 - Redis 是否要求 TLS,端口和
REDIS_ENABLE_TLS是否匹配。 - 托管服务是否限制了 VPS IP。
- 免费数据库的连接数上限是否低于当前连接池设置。
.env中带$的值是否被 Compose 当成变量展开。- VPS 是否能解析并访问托管服务主机名。
日志有可能包含自动生成的管理员口令或连接错误细节。向别人求助前必须脱敏。
第十部分:用 Caddy 提供 HTTPS
这一步的目标是让公网只接触 Caddy,Caddy 再访问本机 8080。
先备份现有配置:
sudo cp -a /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.backup.$(date +%Y%m%d%H%M%S)"
编辑配置:
sudoedit /etc/caddy/Caddyfile
如果服务器上还有博客或其他站点,不要覆盖它们,只新增一个独立站点块:
api.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:8080
}
将 api.example.com 换成自己的真实域名。Caddy 原生支持 WebSocket,并会正确处理常见的流式响应,不需要为了 SSE 复制来历不明的超时配置。
格式化、验证并加载:
sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
sudo systemctl is-active caddy
只有 validate 成功后才能 reload。若 reload 失败,立即查看:
sudo journalctl -u caddy -n 100 --no-pager
Caddy 自动申请证书需要满足:
- 域名 A 记录已经指向当前 VPS。
- 80 和 443 在云防火墙与 UFW 中都已放行。
- 没有错误的 AAAA 记录把访问引向另一台机器。
- 80 或 443 没有被其他程序占用。
第十一部分:逐层验收
不要只看“网页能打开”。按从内到外的顺序检查,才能知道故障在哪一层。
服务器本机
curl --fail --silent --show-error http://127.0.0.1:8080/health
docker compose ps
sudo systemctl is-active caddy
sudo ss -lntp | grep -E ':(22|80|443|8080)[[:space:]]'
预期结果:Sub2API healthy、Caddy active、8080 仅为 loopback、80/443 由 Caddy 监听。
公网 HTTPS
在 Windows PowerShell 中执行:
curl.exe -i --connect-timeout 10 https://api.example.com/health
Windows PowerShell 中可明确写 curl.exe;Ubuntu 中应使用 curl。在 Ubuntu 里输入 curl.exe 会得到 command not found,这不是网络故障。
公网健康接口也必须返回 200。若本机 8080 正常而公网失败,问题通常位于 DNS、UFW、云防火墙、Caddy 或证书层。
管理后台
浏览器打开:
https://api.example.com/
使用 .env 中设置的管理员邮箱和管理员口令登录。进入后台后再添加自己有权使用的上游账号、分组和下游 API Key。
客户端通常填写:
Base URL: https://api.example.com/v1
API Key: 在 Sub2API 后台生成的下游 Key
不同客户端会自动追加 /responses、/chat/completions 等路径,不要在不清楚客户端行为时重复拼接路径。
第十二部分:常见问题与判断方法
1. 443 端口连接被拒绝
先在服务器执行:
sudo ss -lntp | grep -E ':(80|443)[[:space:]]'
sudo systemctl status caddy --no-pager
如果没有监听,先修复 Caddy。若有监听,再检查 DNS、UFW 和云防火墙。
2. 登录后立即回到登录页
依次检查:
- 容器是否正在重启。
JWT_SECRET是否固定且长度足够。- 修改 JWT 后是否清除了浏览器旧站点数据并重新登录。
- 系统时间是否正确。
docker inspect -f '{{.State.Health.Status}} restart_count={{.RestartCount}}' sub2api
timedatectl status
不要把 JWT 值贴到聊天中。
3. 客户端提示本地 502
若错误 URL 是 http://127.0.0.1:<某端口>/...,502 可能来自 CC Switch 或其他本地转发程序,而不是 VPS。
按三步隔离:
- 浏览器或
curl.exe直接访问公网/health。 - 服务器上访问
127.0.0.1:8080/health。 - 最后再检查本地代理软件、TUN 模式和规则。
公网健康正常但本地工具失败,应优先检查本地工具选择的 Provider、Base URL、模型名和代理链路。
4. 开启 Clash 后响应变慢或重连
全局代理或 TUN 模式可能让原本直连的 API 域名改走远端节点,也可能改变 WebSocket、HTTP/2 和长连接路径。切换网络出口时,正在进行的流式连接会被重建。
排查时比较:
- 关闭代理后的公网健康延迟。
- 开启代理后的公网健康延迟。
- 浏览器访问与 API 客户端访问是否经过同一路径。
- 本地
127.0.0.1流量是否被错误接管。
不要同时叠加多个全局代理内核。需要规则时,应只对明确域名做可解释的路由,并保留本地回环地址直连。
5. 域名有时能开、有时失败
重点检查:
Resolve-DnsName api.example.com -Server 1.1.1.1
Resolve-DnsName api.example.com -Type AAAA -Server 1.1.1.1
错误 AAAA、运营商 DNS 缓存、代理软件 DNS 劫持和跨境链路波动都可能造成间歇问题。不要只用一次浏览器刷新判断服务器健康。
第十三部分:日常运维
常用检查
cd ~/sub2api
docker compose ps
docker compose logs --tail=100 sub2api
curl --fail --silent --show-error http://127.0.0.1:8080/health
sudo systemctl is-active caddy
free -h
df -h /
日志持续增长时要检查磁盘。不要等磁盘达到 100% 才处理。
更新前先做什么
至少保存:
- 当前
docker-compose.yml。 - 权限为 600 的
.env安全备份。 - 当前镜像 digest。
- PostgreSQL 的可恢复备份。
- 托管 Redis 的持久化或导出策略说明。
查看当前镜像信息:
docker image inspect weishaw/sub2api:latest --format '{{json .RepoDigests}}'
确认上游发布说明和数据库迁移要求后再更新:
cd ~/sub2api
docker compose pull
docker compose up -d
docker compose ps
curl --fail --silent --show-error http://127.0.0.1:8080/health
不要使用 docker compose down -v,其中 -v 会删除 Compose 管理的数据卷。standalone 模式的主要业务数据在外部 PostgreSQL,但应用卷、配置和日志仍可能有用。
回滚原则
更新前记录旧镜像 digest。新版本失败时,先收集日志,再把 Compose 中的镜像改回已验证的旧 digest,重新创建容器并验证健康接口。不要在没有数据库备份的情况下尝试逆向数据库迁移。
第十四部分:安全检查清单
上线前逐项确认:
- SSH 使用公钥,私钥从未上传到服务器网页之外的第三方位置。
- UFW 和云防火墙只开放 22、80、443。
- 8080 仅监听
127.0.0.1。 - PostgreSQL 和 Redis 不在 VPS 公网监听。
-
.env权限为 600,且没有进入 Git 仓库。 - 管理员口令、数据库口令、Redis 口令和 JWT 随机值互不相同。
-
JWT_SECRET固定,容器重启后登录仍正常。 - Caddy 配置通过 validate,HTTPS 健康接口返回 200。
- 容器状态 healthy,重启次数没有持续增加。
- 已建立数据库备份和更新回滚方法。
- 只添加获授权的上游账号,并遵守相关服务条款。
- 向他人求助前已从日志和截图中删除口令、Token、Cookie 和 Authorization。
最后总结
这套方案能在较低成本下运行,是因为把最吃常驻内存的 PostgreSQL 和 Redis 移到了外部服务。真正决定安全性和稳定性的不是“容器启动成功”,而是以下四条边界始终成立:
- 公网只开放 SSH 和 HTTPS 必需端口。
- Sub2API 的 8080 永远只监听本机。
- 所有长期秘密固定、分离并安全保存。
- 每次修改都从容器健康、回环健康、Caddy、DNS到客户端逐层验证。
做到这些以后,服务器故障和本地代理故障就不会混在一起,后续更新、迁移和排障也会清晰很多。


评论
评论使用 GitHub Discussions 保存,点击后加载。