写一个能长期跑住的服务,nohup ... & 只能算临时方案:重启不恢复、日志没人管、进程挂了不会拉起来。systemd 才是标准答案。

最小可用的 unit

/etc/systemd/system/loach-view-api.service:

[Unit]
Description=LoachBlog 访问量服务
After=network.target postgresql.service

[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/astro-loachblog-server
ExecStart=/usr/bin/node ./src/server.mjs
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
Environment=TZ=Asia/Shanghai

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload      # ⚠ 改完 unit 必须执行,否则不生效
sudo systemctl enable --now loach-view-api
systemctl status loach-view-api

三个最容易踩的坑

① WorkingDirectory 决定“读得到哪个配置文件”。 dotenv 之类的库是按进程工作目录找 .env 的。不设 WorkingDirectory,进程的工作目录会是 /,于是配置静默读不到、服务用默认值启动——表现是“我明明配了却没生效”。这类故障最难查,因为服务看起来是正常启动的。

② 环境变量与登录 shell 完全隔离。 ~/.bashrc 里的 export 对 systemd 服务不可见。要么写进 unit 的 Environment=,要么用 EnvironmentFile=/etc/default/xxx。

③ 时区默认可能是 UTC。 TZ=Asia/Shanghai 要显式设置,否则“按天”的逻辑(比如访问量去重、日志切分)会整体偏移 8 小时。

优雅重启与信号

KillSignal=SIGTERM      # 默认就是它
TimeoutStopSec=15       # 超过这个时间强制 SIGKILL

服务端要监听 SIGTERM 并在此期间停止接受新连接、处理完在途请求、释放连接池。如果进程收到 SIGTERM 就立刻退出,正在写数据库的请求会被腰斩。

排障:journalctl

journalctl -u loach-view-api -n 100 --no-pager    # 最近 100 行
journalctl -u loach-view-api -f                   # 实时跟踪
journalctl -u loach-view-api --since '10 min ago' # 按时间
journalctl -u loach-view-api -p err               # 只看 error 级别

排查顺序建议:status(是否 running、退出码)→ journalctl -n 100(启动横幅与报错)→ 手工以同一身份、同一工作目录执行 ExecStart 的命令复现。

与 pm2 的关系:pm2 用起来更快(pm2 save + pm2 startup),但它自己的守护进程会另起一套“应用列表”,多个 pm2 实例并存时 pm2 list 只能看到一个,容易留下“隐形进程占着端口”的隐患。系统级常驻服务用 systemd 更可控。