AI 项目最常见的部署事故不是“模型突然坏了”,而是本地、CI/CD、部署平台、Docker 容器和远程服务器拿到的环境变量、DNS、出口 IP、超时限制根本不是一套东西。本地能跑,只说明本地这一层通了,不代表生产环境也通。
本文只讨论本人项目的正常开发、构建和部署排障。不要把完整 API Key、代理订阅、Cookie、SSH 私钥、生产日志或用户原始数据发给 AI 或贴到公开工单;如果密钥已经泄露,先轮换或吊销,再清理代码、日志和 Git 历史。
🎯 这篇适合谁
- 本地调用 OpenAI / Claude / Gemini API 正常,但 GitHub Actions、Vercel、Docker 或 VPS 上失败。
- 部署平台里 Preview 正常、Production 失败,或者反过来。
- 你看到 `401`、`403`、`429`、`fetch failed`、`ETIMEDOUT`,但不知道该查 Key、网络还是平台限制。
- 你需要给 AI 项目建立一套可复用的部署排障清单,而不是每次靠猜。
🧭 先建立一张执行环境地图
第一件事不是改代码,而是把“命令在哪里执行”画清楚。AI API 调用可能发生在浏览器、本地开发服务器、CI runner、部署平台函数、Docker 容器、远程 VPS、后台队列或定时任务里。每一层都可能有自己的环境变量和网络出口。
| 环境 | 变量来源 | 排查重点 |
|---|---|---|
| 本地电脑 | .env.local / shell 环境变量 / IDE 配置 | 适合开发调试,不应提交到 Git。 |
| WSL / Remote SSH | 远程 shell、远程项目目录、远程 .env | 和本机 Windows / macOS 不是一套环境。 |
| GitHub Actions | Repository Secret、Environment Secret、workflow env | 受分支、环境保护和 job 配置影响。 |
| 部署平台 | Preview / Production / Development 环境变量 | 不同环境可能各有一份变量。 |
| Docker build | ARG、BuildKit secret、构建阶段网络 | 不要把运行时 API Key 烤进镜像层。 |
| Docker runtime | docker run -e、compose env_file、平台注入变量 | 真实 API 调用通常发生在这里。 |
| VPS systemd | service 文件里的 Environment / EnvironmentFile | 手动 SSH 终端有变量,不代表服务进程也有。 |
🧪 常见现象快速判断
本地 `npm run dev` 能调用 API,GitHub Actions 里 401
CI Secret 没配置、变量名不一致、保护环境未授权,或代码读取了另一个变量名。
Vercel Preview 正常,Production 失败
Preview / Production 环境变量不同,或只在一个环境里更新了 Secret。
构建时能通过,运行时 API 超时
Build 环境和 Runtime 环境不是同一个网络;要在真实运行时打探针。
Docker 宿主机能通,容器内超时
容器没有继承宿主机代理、DNS 或网络配置。
VPS 本地 curl 能通,应用代码超时
应用运行用户、systemd 环境、容器网络、超时参数或 IPv6 和手动终端不同。
日志里只看到 `fetch failed`
日志信息太少;需要补充状态码、错误类型、运行环境、出口 IP 和 request id。
CI 里测试偶发 429
并发太高、重试太激进、同一 Key 被多环境共享,或没有做队列和退避。
🔎 第一步:确认环境变量真的进了运行时
不要先复制新 Key。先确认代码运行的那个环境是否真的读到了变量。排查时只打印变量是否存在和长度,不要输出完整密钥。
# Node.js:只检查变量是否存在和长度,不打印完整 Key
node -e "for (const k of ['OPENAI_API_KEY','ANTHROPIC_API_KEY','GEMINI_API_KEY']) { const v = process.env[k]; console.log(k, v ? 'set len=' + v.length : 'missing') }"
# Linux / macOS / VPS
printenv | grep -E 'OPENAI|ANTHROPIC|GEMINI|HTTP_PROXY|HTTPS_PROXY|ALL_PROXY' | sed 's/=.*/=<redacted>/'
# Windows PowerShell
Get-ChildItem Env: | Where-Object { $_.Name -match 'OPENAI|ANTHROPIC|GEMINI|PROXY' } | ForEach-Object { "$($_.Name)=<redacted>" }
如果本地存在、CI 缺失,就去查 GitHub Actions Secret;如果 Preview 有、Production 没有,就去查部署平台的环境分组;如果 SSH 终端有、systemd 服务没有,就去查 service 文件或 EnvironmentFile。
🔐 第二步:区分本地 .env、CI Secret 和部署平台变量
`.env.local` 只属于本地开发。CI/CD 和部署平台不会自动读取你电脑上的 `.env.local`。正确做法是分别在对应平台配置 Secret,并在 workflow、构建命令或运行时里显式注入。
name: ai-smoke-test
on:
workflow_dispatch:
jobs:
smoke:
runs-on: ubuntu-latest
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: node -e "console.log('OPENAI_API_KEY', process.env.OPENAI_API_KEY ? 'set' : 'missing')"
- run: curl -i https://api.openai.com/v1/models -H "Authorization: Bearer $OPENAI_API_KEY"
- GitHub Actions 的 Secret 不会自动出现在所有 job 里,要通过 `env` 或 action 配置传入。
- 使用 GitHub Environment 时,要确认 workflow 绑定了对应 environment,且保护规则允许本次运行。
- 部署平台通常区分 Development、Preview、Production,更新一个环境不会自动同步到另一个环境。
- 改完环境变量后,许多平台需要重新部署,旧实例不会自动拥有新变量。
🌐 第三步:检查 CI / 生产环境的出口 IP 和 DNS
本地出口 IP 不能代表 CI runner、Vercel Function、Docker 容器或 VPS 生产进程的出口。真实调用在哪里发生,就在哪里打探针。
# 在 CI、VPS、容器和部署平台的真实运行时分别执行
hostname
date
curl -4 https://ipinfo.io
curl -6 https://ipinfo.io || true
nslookup api.openai.com
curl -4 -I https://api.openai.com/v1/models
curl -6 -I https://api.openai.com/v1/models || true
# 只看环境是否有代理变量,不打印账号密码
env | grep -i proxy | sed 's/=.*/=<redacted>/'
- 如果 `curl -4` 通、`curl -6` 不通,IPv6 可能是偶发超时根因。
- 如果 DNS 解析失败,先查运行环境的 DNS,而不是继续换 API Key。
- 如果返回 401,说明网络已经到 API 入口,下一步看认证。
- 如果返回 429,重点看额度、并发、重试和多个环境共用 Key。
📦 第四步:Docker 构建阶段和运行阶段分开查
Docker 里最容易把 build-time 和 runtime 混在一起。构建镜像时需要安装依赖,不等于运行容器时能调用 AI API;也不要为了方便把真实 API Key 写进 Dockerfile 或 build arg。
# 构建阶段:不要用 ARG 固化真实 API Key
docker build -t ai-app .
# 运行阶段:把变量注入容器
docker run --rm \
-e OPENAI_API_KEY="$OPENAI_API_KEY" \
ai-app node -e "console.log(process.env.OPENAI_API_KEY ? 'key set' : 'missing')"
# Docker Compose 示例
services:
app:
image: ai-app
env_file:
- .env.production
environment:
NODE_ENV: production
如果应用在容器内运行,就必须在容器内测试 `curl ipinfo.io`、DNS 和 API 连通性。宿主机通不等于容器通。
⏱️ 第五步:超时、重试和函数运行时限制
部署平台、Serverless Function、CI job、反向代理和应用代码都有自己的超时限制。AI API 请求比普通接口更容易触发长尾延迟,所以生产代码要有明确超时、有限重试和可读日志。
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30_000);
try {
const response = await fetch("https://api.openai.com/v1/models", {
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
signal: controller.signal,
});
console.log(response.status, response.headers.get("x-request-id"));
} finally {
clearTimeout(timeout);
}
排查时要记录“谁先超时”:是应用 fetch 超时、平台函数超时、反向代理超时,还是 API 服务端返回 5xx。不同位置的修法完全不同。
🧩 GitHub Actions / Vercel / VPS 的排查重点
GitHub Actions
重点看 secrets 是否传入 job、是否使用了 environment protection、fork PR 是否拿不到 Secret、matrix 并发是否触发 429。
Vercel / 部署平台
重点看 Preview / Production 环境变量是否分别设置,改完变量后是否重新部署,函数超时和 Edge / Node Runtime 是否一致。
远程 VPS
重点看 systemd 服务环境、Docker 容器环境、服务器 DNS、IPv6、出站防火墙和服务商出口质量。
Docker
重点区分 build-time 和 runtime,不要把 Secret 写入 Dockerfile、镜像层或公开构建日志。
如果是自管 VPS,还要特别注意 systemd。手动 SSH 进去 `echo $OPENAI_API_KEY` 有值,不代表后台服务进程也有值。
# 查看服务实际环境,不要把完整 Key 打进日志
systemctl show your-app.service --property=Environment
# /etc/your-app/your-app.env
OPENAI_API_KEY=sk-...
NODE_ENV=production
# your-app.service
[Service]
EnvironmentFile=/etc/your-app/your-app.env
ExecStart=/usr/bin/node /srv/your-app/server.js
📝 日志怎么留才安全
日志要能定位问题,但不能泄露密钥。生产日志里建议记录运行环境、状态码、错误类型、request id、尝试次数和时间,不记录 Key、Cookie、完整提示词、代理账号密码。
// 推荐记录
{
"runtime": "vercel-production",
"provider": "openai",
"status": 429,
"error_type": "rate_limit",
"request_id": "req_xxx",
"node_version": "22.x",
"attempt": 2
}
// 不要记录
{
"authorization": "Bearer sk-...",
"cookie": "...",
"proxy_url": "http://user:password@host:port",
"full_prompt": "含用户隐私或业务数据的完整提示词"
}
✅ 一份可复制的排障 Runbook
定位执行位置
本地、CI、Preview、Production、VPS、Docker 容器分别记录,不能混用本地结论。
确认变量存在
只打印 set / missing / 长度,不输出完整 API Key。
确认变量环境
检查是不是 Preview 有、Production 没有;Repository Secret 有、Environment Secret 没有。
确认出口 IP
在真实运行时执行 `curl ipinfo.io`,不要只测本地。
确认 DNS / IPv6
分别测试 `curl -4` 和 `curl -6`,IPv6 不稳定时先强制 IPv4 对比。
确认 API 层
能拿到 401 / 403 / 429 / 5xx 时,按状态码排查,不再盲目换节点。
确认超时限制
看函数平台、CI job、反向代理、应用 fetch timeout 和上游 API timeout。
脱敏留证据
保存状态码、request id、运行环境、时间和错误类型;Key 泄露先轮换再清理。
⚠️ 不要这样修
不要把 Secret 打到日志里
排查时只看变量是否存在、长度和前后环境,不要输出完整 Key、Cookie 或代理账号密码。
不要把生产 Key 共用到所有环境
本地、CI、Preview、Production 尽量分开 Key 或至少分开项目,方便限流、审计和轮换。
不要用 `ARG` 固化运行时 Key
Docker 构建层可能被缓存和分发,运行时 Secret 应在容器启动或平台运行时注入。
不要把 429 当网络故障
429 是额度、限流或并发问题,继续换 DNS / 代理通常无效。
不要关闭 TLS 校验
关闭证书校验只会掩盖中间人、公司代理或证书链问题,不适合正式项目。
不要用来路不明 API 中转
正式项目应优先官方 API、明确的网络出口和可审计日志。
🧭 站内推荐路径
Key 和 .env:API Key 与 .env 安全
查看 ->把本地、远程 VPS、CI/CD 和部署平台 Secret 的边界理顺。
错误码分层:AI API 连通性与错误码排查
查看 ->继续区分超时、401、403、429、5xx 和服务端错误。
终端不一致:AI 工具终端网络排查
查看 ->排查浏览器、终端、Docker、Remote SSH、npm 和 git 出口不同。
远程开发机:海外 VPS 开发机初始化清单
查看 ->把 Node、Git、Docker、防火墙、备份和 .env 隔离先搭稳。
稳定出口:稳定 AI 网络环境指南
查看 ->从机场、固定 IP、VPS 落地和海外开发机里选长期方案。
看实测机器:VPS 测评汇总
查看 ->用已有截图和脚本证据判断机器是否适合 AI 开发与部署。
📚 资料来源
- OpenAI API Error codes:用于区分认证、限流、服务端错误和网络超时的排查方向。
- GitHub Actions secrets 文档:用于核对 Actions Secret 的配置和使用边界。
- GitHub Actions variables 文档:用于核对 workflow 里的变量和上下文。
- Vercel Environment Variables 文档:用于核对不同部署环境的变量配置口径。
- Docker daemon proxy 文档:用于核对 Docker 守护进程代理配置。