阅读提要
不用 pm2、不用 Docker,systemd 本身就能把一个 Node 服务管得足够稳:自动重启、资源上限、日志归集、开机自启。关键是把 Unit 文件里那几行写对。
先说结论
一个中等流量的 Node 服务,不需要 pm2 也不需要容器编排。systemd 提供的进程守护能力已经够用,而且它是操作系统自带的一层,出问题时你排查的是内核和 init,不是某个 npm 包的中间层。
代价是:Unit 文件必须写对。写错了不会报错,只会表现为「服务起不来」「重启后数据丢了」「日志找不到」。

一个能直接用的 Unit
[Unit]
Description=blog
After=network.target
[Service]
Type=simple
User=blog
Group=blog
WorkingDirectory=/opt/blog/app/.output/server
EnvironmentFile=/opt/blog/blog.env
ExecStart=/usr/bin/node /opt/blog/app/.output/server/index.mjs
Restart=always
RestartSec=3
KillSignal=SIGTERM
TimeoutStopSec=30
# 资源与权限
MemoryMax=1G
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/opt/blog/data /opt/blog/uploads
[Install]
WantedBy=multi-user.target
几个字段值得单独说。
Type=simple 与 Type=notify:Node 进程不会告诉 systemd「我准备好了」,所以只能用 simple。这意味着 systemd 在 ExecStart 执行后立刻认为服务已启动——如果你的应用要加载十秒才可用,重启风暴期间健康检查会误判。
WorkingDirectory:很多 Node 应用用相对路径读配置和数据库。这个字段缺失时,工作目录是 /,应用会以一个奇怪的路径去找文件,报 ENOENT 却看不出为什么。
EnvironmentFile:环境变量不要写进 ExecStart 那一行。写在文件里,权限设成 600,改配置不用改 Unit,也不用 daemon-reload。
Restart=always + RestartSec=3:崩溃自动拉起。但配上 StartLimitBurst=5 和 StartLimitIntervalSec=60 更稳妥——否则一个必然启动失败的服务会无限重启,把日志刷爆。
日志去哪了
Node 写到 stdout 的内容,systemd 会收进 journald。查日志用:
journalctl -u blog -f # 实时
journalctl -u blog --since "10 min ago"
journalctl -u blog -p err # 只看错误
journald 默认会按大小和磁盘压力自动轮转,不需要自己配 logrotate。但它有两个坑:一是默认 Storage=auto 在 /var/log/journal 不存在时只写内存,重启就没了;二是 journal 的总大小默认限制是磁盘的 10%,小盘机器上可能只留几十 MB。
想长期保留,就建目录并设上限:
# /etc/systemd/journald.conf
Storage=persistent
SystemMaxUse=1G
只重载配置,不重启进程
改 blog.env 之后:
systemctl restart blog
改 Unit 文件本身:
systemctl daemon-reload
systemctl restart blog
忘了 daemon-reload 是新手最常见的错误:改了配置,重启后行为没变,因为 systemd 用的还是内存里的旧 Unit。
几个容易忽略的权限项
NoNewPrivileges=true 阻止进程通过 setuid 提权;PrivateTmp=true 给服务一个独立的 /tmp,避免和其他进程抢临时文件名;ProtectSystem=full 把 /usr、/boot 挂成只读。
但 ProtectSystem 一旦打开,服务就不能往这些目录写。所以必须用 ReadWritePaths 明确放开它真正需要写的目录——数据库、上传目录、缓存目录。漏一个,表现就是「运行几分钟后突然报只读文件系统」。
部署脚本为什么不用手写这些
上面这些配置,手工维护一次没问题,但每次发布都手工 rsync + restart,迟早会漏步骤。所以更稳的做法是把「备份、切换、重启、健康检查、失败回滚」写进一个脚本,Unit 文件只在首次部署时安装一次。
判断标准很简单:如果部署需要你记住五个以上命令,那它就一定会出错。
收尾清单
WorkingDirectory指向应用真正的工作目录,不是仓库根目录;- 环境变量走
EnvironmentFile,权限 600; Restart=always配上启动频率限制;ProtectSystem打开时必须同步配ReadWritePaths;- 部署脚本负责重启与健康检查,人只负责按一次回车。
这五条都满足,一个单机 Node 服务的稳定性就足够了,不必上更重的方案。
你认为这篇文章怎么样?
还没有表情反馈

一起聊聊
一个想法、一点补充,或一句简单的问候。
评论与回复
友善交流,彼此尊重第一句话,留给你。
读完有什么感受?欢迎成为第一个分享的人。