只要你开始在 Linux 上部署服务,迟早都会碰到 systemd。
一开始它通常只是以几个命令的形式出现:
systemctl start xxxsystemctl status xxxsystemctl restart xxx很多人会先把它当成“启动服务的命令工具”来用,这当然没错,但一旦你要自己托管一个程序,很快就会碰到下一层问题:
- 这个服务是怎么定义的?
- 它为什么能开机自启?
- 它崩了为什么会自动重启?
- 环境变量写在哪里?
- 为什么我命令行能跑,到了 systemd 里就不行?
这些问题最后都会指向同一个东西:
systemd 配置文件。
这篇文章我就想把这件事讲清楚,尽量从新手最容易困惑的地方开始,一步步往下拆。
先理解:systemd 配置文件到底是什么
最简单的理解方式是:
它是一份告诉 systemd“该怎么管理某个服务”的说明书。
你可以把它理解成一张服务说明卡,里面会写:
- 这个服务叫什么
- 在什么条件下启动
- 真正执行的命令是什么
- 失败后要不要重启
- 以哪个用户身份运行
- 开机时要不要自动拉起
所以 systemd 并不是凭空知道怎么管理服务的,它是按这些 unit 文件里的规则工作的。
为什么很多人最常接触的是 .service
systemd 支持很多种 unit 类型,例如:
.service.socket.target.mount.timer
但对大多数刚开始部署服务的人来说,最常用的还是:
.service
因为它就是专门用来描述“一个服务进程怎么跑”的。
所以这篇文章先聚焦在 .service 文件上。
一个最小的 service 文件长什么样
先看一个很短的例子:
[Unit]Description=My AppAfter=network.target
[Service]Type=simpleExecStart=/usr/local/bin/my-appRestart=on-failure
[Install]WantedBy=multi-user.target哪怕你现在还没完全看懂,也先别慌。
先记住它通常分成三段:
[Unit][Service][Install]
接下来我们就按这三段来拆。
第一部分:[Unit]
这一段主要描述“这个服务是什么,以及它和其他服务之间的关系”。
Description
这个最好理解,就是给这个服务起一个人类可读的说明。
例如:
Description=Prometheus它会出现在:
systemctl status prometheus的输出里,方便你识别。
After
这个字段不是“依赖谁”,而是更接近:
如果两者都要启动,那我排在谁后面。
例如:
After=network.target意思是:
- 这个服务应该在网络目标之后再启动
这对很多需要网络的服务都很常见。
Wants 和 Requires
这两个容易让新手混淆。
可以先粗略地这样理解:
Wants=:我希望它也一起起来,但它失败了不一定把我一起拖死Requires=:我依赖它,它不行我通常也不该继续
例如:
Wants=network-online.targetAfter=network-online.target这种写法就很常见。
第二部分:[Service]
这一段最重要,因为它决定“这个服务到底怎么跑”。
ExecStart
这是 service 文件的核心字段之一。
它定义了真正启动服务时执行的命令。
例如:
ExecStart=/usr/local/bin/prometheus --config.file=/etc/prometheus/prometheus.yml你可以把它理解成:
- 如果你自己在命令行里手动启动
- 你本来会敲什么命令
- 这里就把那条命令写进去
一个非常常见的误区
很多人第一次写 systemd 文件时,会把命令写成自己在 shell 里能跑的样子,但里面混进了:
- shell 别名
~- 依赖
.bashrc的 PATH - source 之后才存在的环境变量
然后 systemd 里就跑不起来。
这是因为:
systemd 运行服务的环境,和你的交互式 shell 不是一回事。
所以在 ExecStart= 里,尽量写:
- 明确的绝对路径
- 明确的参数
- 少依赖“我终端里刚好有的环境”
Type
这个字段很多人第一次都会跳过,但其实很重要。
最常见的几种是:
Type=simple
最常见,也最适合新手先理解。
表示:
- systemd 启动
ExecStart指定的进程 - 进程起来了,就当服务开始运行
很多普通前台进程都能用它。
Type=oneshot
适合执行一次就结束的任务。
例如:
- 初始化脚本
- 临时准备命令
Type=forking
适合老式 daemon,它启动后会 fork 到后台。
Type=notify
适合支持 systemd 通知机制的程序。
这种会更精细,但新手一开始不用急着碰。
如果你不知道该写什么,很多自己常驻前台运行的程序,先从:
Type=simple开始通常没错。
Restart
这个字段非常实用。
它决定服务退出后要不要自动拉起。
例如:
Restart=on-failure意思是:
- 正常退出不重启
- 异常退出就尝试重启
这是我最常用的一种。
其他常见值还有:
noalwayson-successon-abnormal
如果你托管的是一个长期运行的服务,on-failure 往往就很合适。
User 和 Group
这两个字段决定服务以谁的身份运行。
例如:
User=prometheusGroup=prometheus为什么这很重要?
因为很多服务不应该直接用 root 跑。
如果它只是:
- 监听一个高位端口
- 读自己的配置
- 写自己的数据目录
那通常更好的做法是:
- 给它建一个专门用户
- 只给它需要的权限
这会更安全,也更容易管理权限问题。
WorkingDirectory
有些程序启动时依赖当前工作目录。
例如:
WorkingDirectory=/opt/myapp它的作用就是:
- 启动这个服务时,先进入这个目录
如果你的程序:
- 读相对路径文件
- 依赖当前目录
那这个字段很有帮助。
Environment 和 EnvironmentFile
这两个字段特别实用,因为现实里很多服务都依赖环境变量。
Environment
可以直接写单个变量,例如:
Environment=APP_ENV=productionEnvironment=PORT=8080EnvironmentFile
如果变量很多,更适合单独放到文件里:
EnvironmentFile=/etc/myapp/myapp.env这往往比把一堆变量硬塞进 service 文件里更清楚。
这也是很多人后面会选择的方式。
ExecStartPre 和 ExecStartPost
这两个字段分别表示:
- 启动前先做什么
- 启动后再做什么
例如:
ExecStartPre=/usr/bin/mkdir -p /var/lib/myapp这在某些需要准备目录、检查配置的场景里会很好用。
第三部分:[Install]
这一段很多新手第一次最容易忽略,但它和“开机自启”关系很大。
WantedBy
最常见的是:
WantedBy=multi-user.target你可以先把它理解成:
- 这个服务应该被挂到正常多用户系统启动流程里
然后你执行:
systemctl enable myappsystemd 就会根据这个字段,把它链接到对应 target 下。
换句话说:
没有
[Install],服务也能手动启动,但通常没法正常enable。
一个比较完整、适合新手参考的例子
下面这份 service 文件,已经足够覆盖很多常见场景了:
[Unit]Description=My Python AppAfter=network.target
[Service]Type=simpleUser=myappGroup=myappWorkingDirectory=/opt/myappEnvironmentFile=/etc/myapp/myapp.envExecStart=/usr/bin/python3 /opt/myapp/app.pyRestart=on-failureRestartSec=3
[Install]WantedBy=multi-user.target这个例子大概表达的是:
- 这是一个 Python 服务
- 需要网络起来后再启动
- 用专门用户跑
- 工作目录在
/opt/myapp - 环境变量从单独文件读
- 真正执行的是
python3 app.py - 异常退出后自动重启
- 可以加入开机启动流程
写完 service 文件后,要做什么
很多新手第一次写完 unit 文件之后,会直接 systemctl start,然后发现 systemd 像没看见一样。
这是因为:
写完或者改完 unit 文件后,通常要先 reload。
标准流程一般是:
systemctl daemon-reloadsystemctl enable --now myappsystemctl status myapp --no-pager如果只是改了服务内容但不想立即 enable,也至少要:
systemctl daemon-reloadsystemctl restart myapp最常见的几个命令
查看状态
systemctl status myapp启动
systemctl start myapp停止
systemctl stop myapp重启
systemctl restart myapp开机自启
systemctl enable myapp取消开机自启
systemctl disable myapp重载 systemd 配置
systemctl daemon-reload排错时最有用的两个地方
1. systemctl status
这是你第一眼最该看的。
它通常会告诉你:
- 服务有没有起来
- 最近退出码是什么
- 最近几条日志是什么
2. journalctl
如果 status 不够,就去看日志:
journalctl -u myapp -n 100 --no-pager这个命令对排错非常有用。
如果你发现“命令行能跑,systemd 里不能跑”,十有八九要从这里开始查。
新手最常见的坑
1)ExecStart 写了相对路径
尽量别写:
ExecStart=python app.py更稳的是:
ExecStart=/usr/bin/python3 /opt/myapp/app.py2)依赖 shell 环境
你终端里能跑,不代表 systemd 里也能跑。
比如:
.bashrc里的 PATH- alias
- fnm/nvm/mise
- 代理变量
这些都可能在 systemd 环境里不存在。
3)忘记 daemon-reload
这是特别常见的低级坑。
4)权限不对
如果你指定了 User=myapp,那就要确保:
- 程序文件它能读
- 工作目录它能进
- 日志目录或数据目录它能写
如果你想慢慢学,不用一次记住全部
对新手来说,我建议先记住下面这几个字段:
DescriptionAfterTypeUserWorkingDirectoryEnvironmentFileExecStartRestartWantedBy
只要这几个理解清楚,大多数基础 service 文件你都已经能写出来了。
写在最后
systemd 配置文件一开始看起来像是“很多固定格式的死规则”,但真正写过几次之后,你会发现它本质上只是在回答几个问题:
- 这是什么服务
- 什么时候启动
- 用谁来跑
- 具体跑什么命令
- 出问题后怎么办
你只要按这个思路去看,每个字段都会顺很多。
如果你刚开始接触 Linux 服务托管,我会建议你下一步就自己找一个小程序,给它手写一份 .service 文件,然后跑一遍:
daemon-reloadenable --nowstatusjournalctl
只要你亲手把一个程序交给 systemd 管起来,这套东西就不再只是概念了。