title: "从美国 VPS 到 HTTPS:Vibe Coding Site 上线清单"
slug: deploy-vps-https
publishedAt: 2026-05-13
excerpt: Ubuntu 24.04 + Node 24 + PM2 + Nginx + Let's Encrypt 一条龙;-N '""' 的 passphrase 陷阱、Next.js 16 OG edge runtime 互斥、PubkeyAuthentication no 默认值,踩坑实录而不是教程。
tags: [deploy, vps, https, nginx, letsencrypt]
readingMin: 11
wordCount: 1680
draft: false
上线之前
Sprint 0~3 的代码全部就位之后,这个项目剩下一件事 —— 让 HR 点开链接能看到东西。
本地 localhost:3000 不算交付,简历里挂个 IP 也太业余。所以需要:
- 把整个 monorepo 部署到一台美国 VPS
- 域名(用了一个免费二级域名
bk.aijiaxia123948m.qzz.io)解析过去 - HTTPS 证书 + 自动续期
- 多个 Node 进程 + 反向代理 + 不能跟服务器上已有的服务打架
整个流程从首次 SSH 到 curl -sI https://... 拿到 200,3 小时。
踩了 3 个明显的坑,值得记下来。
第一关:SSH 密钥的 passphrase 陷阱
服务器只给了 root 密码,我要做的第一件事是切到密钥认证。 PowerShell 7 里跑:
ssh-keygen -t rsa -b 4096 -f "J:\密钥\美国质量8-8\vcs-deploy-rsa" -N '""' -C "vcs-deploy"注意 -N '""' —— PowerShell 把 单引号包裹的双引号 当成字面字符串 "" 传给 ssh-keygen,
所以这个密钥实际上是有一个 2 字符的 passphrase(就是两个 " 字符),不是无密码。
第一次知道是踩坑之后:
plink用密码把公钥写到服务器~/.ssh/authorized_keys—— 成功ssh -v -i privkey root@host——debug1: Server accepts key然后立刻Permission denied
服务器认这把公钥,但客户端解锁不了私钥。
看私钥头部的 base64 解码,aes256-ctr / bcrypt KDF 说明真有加密。
修复:-N ""(无引号,空字符串),重生密钥,重推公钥,搞定。
另一个相关的坑:服务器 /etc/ssh/sshd_config 默认 PubkeyAuthentication no。
要么这个 VPS 供应商关掉了,要么是这家 distro 的默认。一句 sed 改 yes + systemctl reload sshd,问题解决。
第二关:服务器环境一条龙
Ubuntu 24.04 LTS,自带的 Node 是 18 或 20,项目要求 Node 24。 最干净的方式是 NodeSource 仓库:
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt-get install -y nodejs
corepack enable
corepack prepare pnpm@10.32.1 --activate
npm install -g pm2四条命令拿到 Node 24.15 + pnpm 10.32.1 + PM2 7.0.1。 不用 nvm,因为 PM2 跑系统服务要走系统 PATH,nvm 那套 shell hook 在 systemd 里别扭。
Nginx 1.24 服务器已经预装,只是没启用我的 vhost,后面再配。
第三关:Next.js 16 的 OG runtime 陷阱
写 OG 图(opengraph-image.tsx)的时候,根目录那个文件挂了 export const runtime = "edge"。
本地 pnpm build 直接报:
Error: Failed to collect configuration for
/blog/[slug]/opengraph-image/[__metadata_id__]
[cause]: Error: Edge runtime is not supported with generateStaticParams.
/blog/[slug]/page.tsx 用了 generateStaticParams(7 篇文章 SSG),
而 Next.js 16 会把 metadata-id 子路由的 runtime 从父级目录继承。
根 OG runtime = "edge" 透到 /blog/[slug]/opengraph-image/[__metadata_id__],与上层 SSG 互斥,build 炸。
修法:删掉根 OG 的 edge runtime,全部走 Node。 SSG 仍然 work,VPS 部署下 Node runtime 和 edge runtime 性能差不多,反正不是 Vercel。
部署链路:tar + scp + build
7 步,每一步都不复杂,串起来就是一条龙:
tar -czf E:\vcs-deploy.tar.gz `
--exclude=node_modules --exclude=.next --exclude=.git `
--exclude=*.log --exclude=dev.db `
-C "E:\web项目" apps deploy package.json pnpm-lock.yaml pnpm-workspace.yaml
scp -i privkey vcs-deploy.tar.gz root@host:/tmp/
ssh -i privkey root@host '
cd /var/www/vcs &&
tar -xzf /tmp/vcs-deploy.tar.gz &&
pnpm install --frozen-lockfile &&
cd apps/api && pnpm exec prisma migrate deploy && pnpm db:seed && pnpm build &&
cd ../web && pnpm build &&
cd /var/www/vcs && pm2 start deploy/ecosystem.config.cjs && pm2 save
'打包后只有 285KB(代码 + 配置,不含依赖)。
服务器端 pnpm install 走 lockfile,31.6 秒装完整个 monorepo;
prisma migrate deploy 应用 init migration;
db:seed 落 14 tags / 7 posts / 3 projects;
tsc 把 apps/api 编译到 dist/;
Next 把 apps/web build 出 21 个预渲染页;
PM2 守护两个进程,pm2 save 落盘。
Nginx 反代 + 不冲突的多服务
这台 VPS 上原本就跑着别的东西:
ss -tlnp | grep LISTEN
# :44193 :8001 -> sing-box (代理)
# :44194 -> 另一个 nginx vhost
# :20241 -> Cloudflare argo tunnel
# :22 -> sshd
# :53 -> systemd-resolved我只动 80 和 443,把项目挂上去:
server {
listen 80 default_server;
server_name bk.aijiaxia123948m.qzz.io 154.9.238.144 _;
location /api/ {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://127.0.0.1:3000;
# 同样 4 个 header
}
}apps/api(Fastify)挂 127.0.0.1:3001,apps/web(Next.js production)挂 127.0.0.1:3000,
两个都只监听 localhost,公网只走 Nginx —— 简洁、可观测、可加 rate-limit。
HTTPS:一行 certbot
域名解析关掉 Cloudflare 橙云(DNS-only,A 记录直指服务器 IP), 然后:
apt-get install -y certbot python3-certbot-nginx
certbot --nginx -d bk.aijiaxia123948m.qzz.io \
--non-interactive --agree-tos --email ... \
--redirect --no-eff-emailcertbot 自己读 nginx 配置找到 server_name 含目标域名的那个 server 块,
通过 HTTP-01 challenge 拿到证书,然后自动改 nginx 加 listen 443 ssl + ssl_certificate paths + 80→443 重定向,reload nginx 完事。
附赠 certbot.timer 自动续期(每天 2 次检查,到期前 30 天自动续),
不需要 cron 不需要 hook。
89 秒后 curl -sI https://bk.aijiaxia123948m.qzz.io/ 拿到 HTTP/1.1 200 OK。
监控的一次"想当然"
部署过程中我开了一个 background bash 在 polling git log 等 Codex 的 commit。
但其实更早的时候 —— Sprint 2 后端那一波 —— 我犯过一个错:
只依赖 stop hook 通知,不主动 git log。
stop hook 触发条件是文件 mtime 变化,Codex 跑 dev 验收时会改 dev.db,这会触发; 但 Codex 在 commit 之后改 md 状态 那段时间,dev.db 不动, hook 沉默 10~20 分钟,我就以为他还在写代码。 实际上他已经 commit 完 3 条微任务进了下一轮。
教训:通知是被动信号,不是全部真相。
任何长跑动作,主流程定期 git log --oneline -5 && git status --short 探一探,比纯等 hook 可靠。
后记
整个站现在挂在 https://bk.aijiaxia123948m.qzz.io/, 21 个静态页 + 11 个 API 端点 + RSS feed + sitemap,加起来 100KB 不到的源码差量; SSL 证书自动续期到 2026 年 8 月; PM2 守护两个 Node 进程,Nginx 在前面拦着。
剩下的事:跑一次真实的 Lighthouse、把 SITE_URL 改成环境变量、
把 feed/sitemap 里写死的 localhost:3000 改掉、
给 Subscriber.token 加一个 @unique 约束 ——
都是部署稳定后的小修小补,不阻塞 demo。
写完这篇,Vibe Coding 这个项目就算第一版真的上线了。