Service Deploy — 服务上线全流程 v2
从"有源码"到"公网可访问且冒烟通过"的标准流程。
适用于:腾讯云轻量/CVM + DNSPod + EdgeOne CDN + React/Vite 前端 + Express BFF 后端。
前置条件
- [ ] 目标服务器 SSH 访问(root,密钥认证)
- [ ] tccli 已配置(腾讯云凭证)
- [ ] 有域名管理权限(DNSPod 或 EdgeOne 接入)
- [ ] Node.js >= 22(前端构建用,服务器 nvm 管理)
- [ ] Playwright 已安装(冒烟测试用)
完整工作流(9 步)
Step 1: 环境检查
# 服务器连接测试
SSH_CMD="ssh -i <KEY> -o StrictHostKeyChecking=no root@<IP>"
$SSH_CMD "uname -a; node -v; nginx -v 2>&1; ss -tlnp | grep ':80\|:443'"
# 检查 Node 版本(Vite 8 需要 Node >= 22)
$SSH_CMD 'source /root/.nvm/nvm.sh && nvm use 22 && node -v'
# 检查端口占用
$SSH_CMD 'ss -tlnp | grep :80'
# 如被占用:systemctl stop <占用服务> && systemctl disable <占用服务>
Step 2: DNS 解析
# 方案A: DNSPod 直解(适合无 CDN)
tccli dnspod CreateRecord --Domain <主域名> --SubDomain <子域名> --RecordType A --RecordLine 默认 --Value <IP>
# 方案B: EdgeOne CDN 接入(推荐,自带 HTTPS + 加速)
# 1. 在 EdgeOne 控制台添加加速域名
tccli teo CreateAccelerationDomain --ZoneId <zone-id> --DomainName <子域名>.<主域名> --OriginType IP_DOMAIN --Origin <IP>
# 2. DNS CNAME 到 EdgeOne
# 在 DNSPod 添加 CNAME: <子域名> → <子域名>.<主域名>.eo.dnse2.com
验证: dig <子域名>.<主域名> +short
Step 3: Nginx 配置
# 写入配置(注意:如果走 EdgeOne CDN,回源协议必须设为 HTTP)
$SSH_CMD "cat > /etc/nginx/conf.d/<子域名>.conf << 'EOF'
server {
listen 80;
server_name <子域名>.<主域名>;
root /opt/<项目名>/client/dist;
index index.html;
# API 反代 + CORS(前后端分离时必需)
location /api/ {
if (\$request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
proxy_pass http://127.0.0.1:<后端端口>;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
}
location / {
try_files \$uri \$uri/ /index.html;
}
}
EOF"
$SSH_CMD 'nginx -t && nginx -s reload'
Step 4: EdgeOne CDN 配置(可选但推荐)
# 关键:设置回源协议为 HTTP(nginx 只监听 80)
tccli teo ModifyAccelerationDomain --ZoneId <zone-id> --DomainName <子域名>.<主域名> --OriginProtocol HTTP
# 申请免费 HTTPS 证书
tccli teo ApplyFreeCertificate --ZoneId <zone-id> --DomainName <子域名>.<主域名>
# 验证回源
curl -sk https://<子域名>.<主域名>/api/<health-path>
⚠️ OriginProtocol 必须为 HTTP:
- 如果设为 FOLLOW/HTTPS,CDN 用 HTTPS 回源但 nginx 只监听 80 → 回源失败 → 返回缓存旧内容或错误页面
- 这是白屏问题的高频根因之一
Step 5: 前端构建与部署
# 用 Node 22 构建(Vite 8 强制要求)
$SSH_CMD 'source /root/.nvm/nvm.sh && nvm use 22 && cd /opt/<项目>/client && npm install && npx vite build'
# 或在本地构建后 scp
# cd /tmp/<project>-client && npm install && npx vite build
# scp -r dist/* $SSH_CMD:"/opt/<project>/client/dist/"
# 清除 CDN 缓存(每次构建后必须执行)
tccli teo CreatePurgeTask --ZoneId <zone-id> --Type purge_host --Targets '["<子域名>.<主域名>"]'
前端常见坑:
| 问题 | 根因 | 修复 |
|---|---|---|
| 白屏 | index.css 是 Vite 脚手架残留模板,覆盖了 Ant Design 样式 |
替换为最小 reset |
| 白屏 | 图标在 Ant Design 6 中不存在(如 NetworkOutlined) |
替换为存在的图标(如 ApiOutlined) |
| 白屏 | CDN 缓存旧 HTML,引用不存在的 JS 文件 | 每次 build 后清 CDN 缓存 |
| 白屏 | EdgeOne 回源协议 HTTPS,nginx 只监听 80 | OriginProtocol = HTTP |
| 样式错乱 | index.css 有 #root { width: 1126px; text-align: center } 等 |
清理脚手架 CSS |
| 构建失败 | Node 18 + Vite 8 | 用 nvm 切 Node 22 |
Step 6: 后端 API 修复(关键:字段映射)
腾讯云 TEO SDK 的返回字段名经常和文档不一致,必须实测:
# 1. 用 tccli 先调通,确认返回字段名
tccli teo DescribeAccelerationDomains --ZoneId <zone-id> --Limit 5
# 发现返回 AccelerationDomains 而非 Domains
tccli teo DescribeDefaultCertificates --ZoneId <zone-id>
# 发现 DescribeCertificates 根本不存在
# 2. 用 SDK 列出所有可用方法
node -e "
const t = require('tencentcloud-sdk-nodejs');
const c = t.teo.v20220901.Client.prototype;
console.log(Object.getOwnPropertyNames(c).filter(m => m.startsWith('Describe')).sort().join('\n'));
"
# 3. 后端做字段映射
# 例:domain.js
# resp.AccelerationDomains → map → { DomainName, Status: DomainStatus, OriginType, Origin }
常见字段不匹配:
| 前端期望 | API 实际返回 | 映射层 |
|---|---|---|
Records |
Zones / Domains / DnsRecords |
后端返回统一字段名 |
Status |
ActiveStatus / DomainStatus |
后端映射为 Status |
Domains |
AccelerationDomains |
后端提取并映射 |
Origin |
OriginDetail.Origin |
后端解包嵌套结构 |
TotalCount + 分页数据 |
TotalCount + 空数组 |
检查 Offset/Limit 参数 |
时间格式(TEO API):
// ✗ 不行:toISOString() = "2026-05-06T07:00:00.815Z"(有毫秒)
// ✗ 不行:toISOString().replace('Z','+08:00') = "2026-05-06T07:00:00.815+08:00"(有毫秒)
// ✓ 正确:
const fmt = (dt) => dt.toISOString().split('.')[0] + '+08:00';
// 结果:"2026-05-06T07:00:00+08:00"
Step 7: 冒烟测试
# === 1. 静态资源 ===
HTML=$(curl -sk https://<域名>)
JS_HASH=$(echo "$HTML" | grep -o 'index-[^.]*\.js' | head -1)
curl -sk -o /dev/null -w "%{http_code}" "https://<域名>/assets/$JS_HASH" # 必须 200
# === 2. API 端点逐个验证 ===
for ep in /api/xxx/list /api/yyy/detail; do
result=$(curl -sk "https://<域名>$ep")
code=$(echo "$result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('code','?'))")
echo "$ep → $code"
done
# === 3. Headless 浏览器渲染测试 ===
node -e "
const {chromium} = require('playwright');
(async () => {
const browser = await chromium.launch({headless:true});
const pages = ['/', '/sites', '/domains', '/dns', '/cache', '/security'];
for (const p of pages) {
const pg = await browser.newPage();
const errs = [];
pg.on('pageerror', e => errs.push(e.message));
await pg.goto('https://<域名>' + p, {waitUntil:'networkidle',timeout:15000});
await pg.waitForTimeout(2000);
const ok = await pg.evaluate(() => document.getElementById('root').children.length > 0);
console.log(p.padEnd(15), ok ? '✓' : '✗', errs.length ? 'ERR:'+errs.length : '');
await pg.close();
}
await browser.close();
})();
"
Step 8: CNB 仓库同步
# 克隆仓库
git clone https://cnb:$CNB_TOKEN@cnb.cool/<org>/<repo>.git /tmp/<repo>
cd /tmp/<repo>
# 从服务器拉取最新代码覆盖
SSH_CMD="ssh -i <KEY> -o StrictHostKeyChecking=no root@<IP>"
scp -r $SSH_CMD:/opt/<项目>/client/src/ client/src/
scp -r $SSH_CMD:/opt/<项目>/server/routes/ server/routes/
# 提交推送
git add -A
git commit -m "fix: <修改说明>"
git push
Step 9: 清理
# 清理临时文件
rm -rf /tmp/<project>-* /tmp/test-page*.mjs
# 更新 MEMORY.md 中的部署信息
# 记录:域名、IP、端口、服务名、nginx 配置路径、EdgeOne ZoneId
关键提示词模板
| 场景 | 提示词 |
|---|---|
| 部署新服务 | "把 <项目> 部署到 <IP>,用 <子域名>.<域名>,后端端口 <端口>" |
| 白屏排查 | "访问 <域名> 白屏,查看源码排查" |
| 数据不显示 | "前端没有显示真实数据,检查 API 字段映射" |
| 冒烟测试 | "对 <域名> 做完整冒烟测试,所有 API + 所有页面" |
| 更新仓库 | "把修改同步到 CNB <org>/<repo> 仓库" |
| CDN 缓存 | "刷新 EdgeOne CDN 缓存,<域名>" |
常见问题速查
| 问题 | 排查命令 | 根因 |
|---|---|---|
| 白屏 | 浏览器 F12 看 Console | JS 运行时报错(图标/CSS/API) |
| 白屏 | curl -s https://域名 \| grep index- |
CDN 缓存旧 HTML,JS hash 不匹配 |
| 白屏 | curl -sk https://域名/api/xxx |
CDN 回源 HTTPS 失败,返回 HTML fallback |
| API 返回空数组 | tccli teo <API> --ZoneId <id> --Limit 5 |
字段名不对(Domains vs AccelerationDomains) |
| 前端数据全 0 | 检查 API 返回的 key 名 | 前端取 Records 但后端返回 Zones |
| 构建失败 | node -v + nvm use 22 |
Vite 8 要求 Node >= 22 |
| API 时间参数错误 | toISOString().split('.')[0] + '+08:00' |
TEO 不接受带毫秒的时间 |
| nginx 404 | 检查 root 和 try_files |
静态文件路径配置错误 |
实际案例
case1: design.yunjinhong.cn(简单部署)
- 服务器:175.178.24.59 | 后端:uvicorn 8080 | Nginx: 80
- 无 CDN,无前端框架,单页 HTML
case2: eo.yunjinhong.cn(全栈 + CDN + 冒烟)
- 服务器:175.178.24.59 | 后端:Express 3001 | 前端:React + Ant Design 6 + Vite 8
- EdgeOne CDN(ZoneId: zone-3pjwxe4xqjlo)| HTTPS: eofreecert
- CNB 仓库:cnbnn/edgeone-dashboard
- 踩坑:白屏(NetworkOutlined + 残留 CSS + CDN 回源协议)、API 字段映射、时间格式
- 冒烟结果:7/7 API ✓,8/8 页面 ✓,Dashboard 显示 站点2/域名15/DNS0/证书1
注意事项
- 每次构建后必须清 CDN 缓存,否则用户看到的还是旧版
- EdgeOne OriginProtocol 必须设 HTTP(除非 nginx 配了 443)
- 腾讯云 SDK API 名称可能和文档不一致,用
Object.getOwnPropertyNames(Client.prototype)实查 - Vite 8 强制 Node >= 22,systemd 的 ExecStart 也要指向正确的 node 路径
- 前后端字段映射要在后端做,不要在前端适配各种奇怪的 API 字段名
- Playwright 冒烟测试是最可靠的验证手段,比 curl 更接近真实用户体验