先选对 Type
大多数自己写的 HTTP 服务用 Type=simple 就够:进程在前台跑,systemd 认为主进程退出即服务退出。只有程序会自己 fork 到后台时,才考虑 forking,并且一定要配 PIDFile=。
如果进程启动后还要等端口就绪,用 Type=notify 并在程序里发 READY=1。不要用 sleep 假装就绪,重启时序一变就会踩空。
一份常用模板
我把长期跑的 Python 服务放在 /opt/notes-api,用独立用户,不给 login shell。unit 大致如下:
[Unit]
Description=notes api
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=notes
Group=notes
WorkingDirectory=/opt/notes-api
EnvironmentFile=/etc/notes-api.env
ExecStart=/opt/notes-api/.venv/bin/python -m app
Restart=on-failure
RestartSec=3
NoNewPrivileges=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
EnvironmentFile 比在 unit 里堆 Environment= 好改。文件权限给 0640,属主 root,组给运行用户。
失败时看 journal
先 systemctl status notes-api,再 journalctl -u notes-api -n 80 --no-pager。status 只能告诉你退出码,真正的 ImportError、权限错误、地址占用都在 journal 里。
常见三类:
- 退出码 203/EXEC:可执行文件路径错,或没有执行权限。
- 权限拒绝:WorkingDirectory 对 User 不可读,或日志目录属主不对。
- 立刻重启循环:程序崩在导入阶段,Restart=always 会把日志刷没。先改成 no,修到能起来再打开。
把 ExecStart 写成一长串环境变量,是我以前最差的习惯。变量一多,报错位置就看不见了。
Restart 不要写太猛
Restart=always 会把配置错误也当成可恢复故障。生产里我用 on-failure,并加 StartLimitBurst=5 与 StartLimitIntervalSec=60,避免配置写错时打满磁盘。
改完 unit 记得 systemctl daemon-reload。只 restart 不会重新读文件,这点我每年至少忘一次。