用 systemd 托管一个常驻服务

背景

手上有一个小程序需要长期运行,比如一个监听本地端口的 Python 脚本。直接用 nohup 放到后台虽然能跑,但进程崩溃后不会自动拉起,重启机器后也要手动启动,日志还散落在各处。

现代 Linux 发行版大多使用 systemd 作为初始化系统,把程序交给它管理,就能一次性解决 开机自启、异常重启、统一日志这三个问题。下面以 /opt/hello/app.py 为例。

1. 创建专用用户

服务不需要 root 权限时,就不要用 root 运行。先创建一个不能登录的系统用户:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin hello
sudo chown -R hello:hello /opt/hello

2. 编写 unit 文件

自定义的 unit 文件放在 /etc/systemd/system/ 下,文件名就是服务名:

# /etc/systemd/system/hello.service
[Unit]
Description=Hello demo service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=hello
Group=hello
WorkingDirectory=/opt/hello
ExecStart=/usr/bin/python3 /opt/hello/app.py
Environment=PORT=8080
Restart=on-failure
RestartSec=5s

# 基础加固
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

几个关键字段:

  • ExecStart:必须写绝对路径,systemd 不会读取你的 Shell 环境变量 PATH。
  • Restart=on-failure:进程非正常退出时自动重启,RestartSec 控制间隔。
  • ProtectSystem=strict:整个文件系统对该服务只读。程序如果需要写文件,用 ReadWritePaths=/var/lib/hello 单独放行。
  • WantedBy=multi-user.target:决定执行 enable 时服务挂在哪个启动目标下,普通服务用这个即可。

3. 启动与开机自启

sudo systemctl daemon-reload        # 让 systemd 读取新文件
sudo systemctl enable --now hello   # 设为开机自启并立即启动
systemctl status hello              # 查看运行状态

status 输出里重点看 Active: 这一行,active (running) 表示正常运行;下方还会附带最近几行日志,排错时很有用。

4. 查看日志

程序写到标准输出和标准错误的内容,会被 journald 自动收集:

journalctl -u hello -f                    # 实时跟踪
journalctl -u hello --since "1 hour ago"  # 最近一小时
journalctl -u hello -b                    # 本次开机以来
journalctl -u hello -p err                # 只看错误级别

Python 默认会缓冲标准输出,日志可能迟迟不出现。可以在 unit 中加上 Environment=PYTHONUNBUFFERED=1,或者用 python3 -u 启动。

5. 修改配置的正确姿势

对于发行版软件包自带的服务,不要直接改 /usr/lib/systemd/system/ 下的文件,升级时会被覆盖。推荐使用 drop-in 覆盖:

sudo systemctl edit hello      # 打开编辑器,写入要覆盖的片段
sudo systemctl restart hello

保存后会生成 /etc/systemd/system/hello.service.d/override.conf,并自动重新加载配置。需要覆盖 ExecStart 时要先写一行空的 ExecStart= 清空原值,再写新值。用 systemctl cat hello 可以看到合并后的最终配置。

常见问题

现象常见原因
status=203/EXECExecStart 路径错误、不是绝对路径,或文件没有执行权限。
status=217/USERUser= 指定的用户不存在。
status=200/CHDIRWorkingDirectory 不存在或没有访问权限。
反复重启后停止触发了启动频率限制,修复问题后执行 systemctl reset-failed hello 再启动。
改了文件但不生效忘记执行 daemon-reload,或者被某个 drop-in 覆盖了。

小结

一个常驻服务交给 systemd 管理,最少只需要 [Unit]、[Service]、[Install] 三段配置。养成“专用用户 + 绝对路径 + 自动重启 + 基础加固”的习惯,后续维护会轻松很多。更多字段可以查阅 man systemd.service 和 man systemd.exec。