VPSKnow

AI 项目本地能跑,CI/CD、Vercel、Docker、远程服务器里为什么失败?

初级-中级
36分钟

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、明确的网络出口和可审计日志。

🧭 站内推荐路径

📚 资料来源

🚀 下一步行动

🚀 下一步行动

部署环境排查清楚后,建议继续补齐网络、密钥和远程开发机三条线:

读完后建议 先验证,再选择
把判断落到具体选择

准备购买 VPS 时,先对照推荐榜单和真实测评确认线路、价格、用途与风险;只是继续学习,可以回到教程索引按主题往下看。