现在把一个 Next.js 站点发到线上,最省事的做法是连上 Vercel,推代码就完了。
我没这么做。StyleKit 跑在我自己的一台阿里云 ECS 上,前面是 Nginx,后面是 systemd 守护的一个 Node 进程,部署靠 rsync。
这篇讲清楚这条链路每一环在干什么,以及为什么「不在生产服务器上 build」是一条必须守住的纪律。
一、它解决什么问题
先说托管平台的问题。把 Next.js 部署到 Vercel 这类平台,你换来的是:不用管服务器、推代码自动构建上线、HTTPS 和 CDN 都是现成的。
代价是:要花钱而且随流量走不可预测;构建有配额和时长限制;服务器在境外,国内访问体验不理想。
我的项目是给自己和国内用户用的,域名和服务器都在国内。托管平台解决的是「我不想碰服务器」这个问题,但我愿意碰。
所以选择自建。自建的核心难题是:没有平台帮你做那些自动化,你得自己定义「部署」这个动作。
二、不用它会怎样
自建的常见翻车方式,基本都出在同一个地方:在生产服务器上跑构建。
想象这个流程:
ssh prod
cd /www/stylekit
git pull
pnpm install # 开始吃内存
pnpm run build # Next.js 构建,内存峰值很高问题在于:Next.js 的构建过程是内存密集型操作,它的峰值内存可能是运行时的好几倍。而生产服务器上同时还跑着正在服务用户的 Node 进程。
结果就是:
- 构建把内存吃满
- 系统开始用 swap,整机变慢
- 用户请求超时
- 严重时 OOM killer 直接把线上进程杀掉
你为了发布一个新版本,把正在服务的旧版本搞挂了。这是最典型的一类自建事故。
所以 CLAUDE.md 里写了一条硬规则:
Never run
pnpm run buildon the production server. The host is memory constrained; if the server needs the artifact, sync the locally verified.next/directory instead.
生产服务器只负责运行,不负责构建。
三、代码是怎么做的
整条链路
本地: 构建 + 验证 → rsync 产物 → 服务器: 重启服务四样东西各司其职:
| 组件 | 角色 |
|---|---|
| rsync | 把本地验证过的产物同步到服务器 |
| systemd | 守护 Node 进程,挂了自动拉起 |
| Nginx | 反向代理,对外接 HTTPS |
| healthcheck timer | 定时探活,连续失败就重启服务 |
systemd:为什么用它守护进程
你当然可以 nohup node server.js & 把进程扔到后台。但这样:机器重启进程就没了、进程崩了没人拉起来、日志散落各处、想重启得手动找 PID。
systemd 是 Linux 的 init 系统,它管的就是「让一个进程按我希望的方式一直活着」:开机自启、崩溃自动重启、日志统一收进 journalctl、systemctl restart 一条命令重启。
我的服务叫 stylekit.service,启动命令是:
next start --hostname 0.0.0.0 --port 13000注意端口是 13000,不是 80 也不是 3000。外部流量根本不直接连这个端口——它由 Nginx 转发进来。Node 进程只监听内网,不对公网暴露。
Nginx:对外的那一层
Nginx 在这里做三件事:
反向代理。用户访问
https://www.stylekit.top,连的是 Nginx 的 443 端口,Nginx 再把请求转给127.0.0.1:13000。用户永远不知道后面是个跑在 13000 端口的 Node 进程。终止 HTTPS。证书配在 Nginx 上,Node 只处理明文 HTTP,应用代码完全不用关心证书。
静态资源直接吃。这条最省资源。
/_next/static/下的文件是内容寻址的(文件名带哈希,内容变了文件名就变),可以长期缓存。配置里直接让 Nginx 从磁盘读:
location ^~ /_next/static/ {
alias /www/stylekit/.next/static/;
add_header Cache-Control "public, max-age=31536000, immutable";
}这些请求根本不经过 Node 进程。用户拿 JS 和 CSS 的时候,Node 完全不用被唤醒。
rsync:为什么只同步产物
rsync 的关键特性是增量同步——只传有变化的部分,不是整个目录重传。但更重要的是「同步什么」这个决定,规则是:
本地构建验证,只同步产物,不在生产服务器上 build。
流程是:本地跑完整验证门禁(lint、类型检查、测试)→ 本地 pnpm run build → 构建通过之后才 rsync .next/ 和代码到 /www/stylekit/ → systemctl restart stylekit。
顺序不能变。只有本地构建成功了,才有东西可以同步。服务器上没有构建这一步,也就不存在构建吃满内存的风险——它接到的永远是已经构建好的、验证过的产物。
为什么这个决定重要
这条规则的价值不在于「省了服务器资源」,而在于它把失败挡在了生产环境之外:构建在本地失败,服务器上什么都没发生,线上还是旧版本,用户无感。把有风险的操作放在不会影响用户的地方执行,这是这条规则的本质。
配套的还有 healthcheck:stylekit-healthcheck.timer 每分钟探测一次 /api/health,连续失败就重启服务。顺带一提,vercel.json 还在仓库里,但 README 明确标注它已经不在这条部署链路上——配置文件残留是很容易误导人的东西。
四、面试官会追问什么
问:为什么不用 Vercel?
答:三个原因。一是成本不可预测,按流量计费;二是构建有配额和时长限制;三是服务器在境外,国内访问体验不好。我的用户主要在国内,服务器也在国内。托管平台解决的是「不想碰服务器」的问题,而自建换来更可控的成本和更近的网络路径。
问:为什么坚决不在生产服务器上 build?
答:Next.js 构建是内存密集型操作,峰值内存可能是运行时的好几倍。生产服务器同时还跑着服务用户的 Node 进程,构建吃满内存会导致 swap、请求超时,严重时 OOM killer 会直接杀掉线上进程。为发布新版本而搞挂旧版本是不可接受的。 所以规则是本地构建验证、只同步
.next/产物。
问:systemd 和
nohup加&有什么区别?答:nohup 只是让进程不受终端退出影响,不提供任何管理能力。systemd 提供进程生命周期的完整管理:开机自启、崩溃自动重启、日志归集到 journalctl、标准化启停命令。关键差别是「崩溃自动重启」——没有它,进程半夜挂了就一直是挂的。
问:Nginx 在这里是不是多余的?Node 直接监听 80 端口不行吗?
答:技术上可以,但会丢掉三件事。一是 HTTPS 终止,证书和 TLS 配置放在 Nginx 比放在应用里更简单也更安全;二是静态资源由 Nginx 直接从磁盘返回,完全不经过 Node 进程;三是 Node 可以只监听内网,不直接对公网暴露,攻击面更小。
问:如果 rsync 传到一半断了会怎样?
答:下一次 rsync 会继续增量同步,已传完的文件不会重传。但要特别注意顺序:必须先同步完文件,再重启服务。先重启再同步的话,服务可能读到一半新代码一半旧代码,这是最危险的中间态。
五、一句话版本
自建部署的核心纪律是「本地构建验证、只同步产物」,因为 Next.js 构建吃内存,在生产服务器上构建可能为发布新版而搞挂正在服务的旧版。
六、自测
- 用自己的话说清 systemd 比
nohup ... &多提供了什么能力。 - Nginx 在这条链路里做了哪三件事?分别对应什么收益?
- 为什么
/_next/static/可以设置一年的缓存时间? - 为什么不能在服务器上 build?具体会发生什么?
- 部署时为什么必须「先同步完文件,再重启服务」?反过来会怎样?