写一个可靠的自启服务单元

2026-09-18 约 1300 字 systemd

把一个自己写的程序做成开机自启的服务,最省事的做法是写一个 unit 文件。 下面是能直接用的最小模板:

[Unit]
Description=My Service
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node /opt/myapp/dist/main.js
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

安装与启用:

sudo cp myapp.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp --no-pager

Type 怎么选

绝大多数常驻进程用 Type=simple:systemd 执行命令后立即认为服务已启动。 如果程序会自己 fork 到后台(典型的老式守护进程),用 Type=forking。

一次性的初始化任务(比如导入数据、清理缓存)用 Type=oneshot, 配合 RemainAfterExit=yes 让它在执行完后仍显示 active。

Restart 策略

取值 行为
no 不重启(默认)
on-failure 非零退出、被信号杀死、超时时重启
always 无论如何都重启,包括正常退出

常驻服务写 on-failure 通常就够。但要注意:配置错误导致的 启动失败也会被当作 failure 并不断重试,这就是下一个坑的来源。

一个把 CPU 和日志一起打满的坑

曾经有两个前端开发服务器的 unit 文件留在机器上,WorkingDirectory 指向的目录后来被删除了。结果是这样:

systemctl status playlink-admin
# Active: activating (auto-restart) (Result: exit-code)
# Main process exited, code=exited, status=200/CHDIR

journalctl -u playlink-admin -n 3
# Changing to the requested working directory failed: No such file or directory
# Failed with result 'exit-code'

status=200/CHDIR 表示切换工作目录失败。因为配了 Restart=always 和 RestartSec=5, 它每 5 秒重启一次,重启计数累积到了 700 次,机器负载一直挂着。

关键点 这类失败在 systemctl --failed 里看不到。 它的状态是 activating (auto-restart),不属于 failed。 要发现它,得查 --state=activating 或直接看负载来源。

排查命令:

# 列出反复重启的服务
systemctl list-units --type=service --state=activating --no-legend

# 看重启次数
systemctl show myapp -p NRestarts -p Result -p ExecMainStatus

处理方式:

sudo systemctl disable --now myapp
sudo rm /etc/systemd/system/myapp.service
sudo systemctl daemon-reload
sudo systemctl reset-failed myapp

最后一步 reset-failed 常被忘掉。删了 unit 文件之后, systemctl is-active myapp 可能还会返回 failed, 因为失败状态被记在内存里。

几个实用的补充项

环境变量

不要把密钥直接写在 unit 里(/etc/systemd/system 通常全局可读), 用独立的环境文件并收紧权限:

[Service]
EnvironmentFile=/opt/myapp/.env

# 同时
chmod 600 /opt/myapp/.env

日志

默认输出会进 journal,用 journalctl -u myapp -f 查看。 如果程序自己有日志文件,注意目录要在 WorkingDirectory 存在的前提下才可写—— 这又回到上面那个坑:目录没了,程序连日志都写不出来,只能靠 journal 看到 CHDIR 错误。

启动顺序依赖

After=network.target 只表示顺序,不表示依赖。 如果服务必须等网络真正可用,用:

After=network-online.target
Wants=network-online.target

资源限制

内存泄漏的程序可以加个上限,超过后由 systemd 重启:

[Service]
MemoryMax=512M
MemoryHigh=384M

检查清单

  • unit 文件放在 /etc/systemd/system/,改完必做 daemon-reload
  • WorkingDirectory 和 ExecStart 里的路径都要用绝对路径
  • 用 systemctl cat myapp 确认 systemd 实际读到的内容
  • 用 systemctl show myapp -p NRestarts 检查有没有在偷偷重启
  • 删除服务时按 disable → rm → daemon-reload → reset-failed 的顺序做
← 返回笔记列表 回到第一篇:Nginx gzip 实测 →