vibecoding.site
← back to blog
约 11 分钟5 标签

从美国 VPS 到 HTTPS:Vibe Coding Site 上线清单

Ubuntu 24.04 + Node 24 + PM2 + Nginx + Let's Encrypt 一条龙;passphrase 陷阱、Next.js 16 OG edge runtime 互斥、与服务器原有服务的不冲突共存 —— 踩坑实录而不是教程。


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; tscapps/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-email

certbot 自己读 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 这个项目就算第一版真的上线了。

评论

加载评论中…

留下评论 · 提交后由 PM 审核显示

评论无需注册,审核通过后可见。