2419 字
12 分钟
systemd 配置文件怎么写:从 service 文件结构到常见字段的详细入门

只要你开始在 Linux 上部署服务,迟早都会碰到 systemd

一开始它通常只是以几个命令的形式出现:

Terminal window
systemctl start xxx
systemctl status xxx
systemctl restart xxx

很多人会先把它当成“启动服务的命令工具”来用,这当然没错,但一旦你要自己托管一个程序,很快就会碰到下一层问题:

  • 这个服务是怎么定义的?
  • 它为什么能开机自启?
  • 它崩了为什么会自动重启?
  • 环境变量写在哪里?
  • 为什么我命令行能跑,到了 systemd 里就不行?

这些问题最后都会指向同一个东西:

systemd 配置文件。

这篇文章我就想把这件事讲清楚,尽量从新手最容易困惑的地方开始,一步步往下拆。

先理解:systemd 配置文件到底是什么#

最简单的理解方式是:

它是一份告诉 systemd“该怎么管理某个服务”的说明书。

你可以把它理解成一张服务说明卡,里面会写:

  • 这个服务叫什么
  • 在什么条件下启动
  • 真正执行的命令是什么
  • 失败后要不要重启
  • 以哪个用户身份运行
  • 开机时要不要自动拉起

所以 systemd 并不是凭空知道怎么管理服务的,它是按这些 unit 文件里的规则工作的。

为什么很多人最常接触的是 .service#

systemd 支持很多种 unit 类型,例如:

  • .service
  • .socket
  • .target
  • .mount
  • .timer

但对大多数刚开始部署服务的人来说,最常用的还是:

  • .service

因为它就是专门用来描述“一个服务进程怎么跑”的。

所以这篇文章先聚焦在 .service 文件上。

一个最小的 service 文件长什么样#

先看一个很短的例子:

[Unit]
Description=My App
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/my-app
Restart=on-failure
[Install]
WantedBy=multi-user.target

哪怕你现在还没完全看懂,也先别慌。

先记住它通常分成三段:

  • [Unit]
  • [Service]
  • [Install]

接下来我们就按这三段来拆。

第一部分:[Unit]#

这一段主要描述“这个服务是什么,以及它和其他服务之间的关系”。

Description#

这个最好理解,就是给这个服务起一个人类可读的说明。

例如:

Description=Prometheus

它会出现在:

Terminal window
systemctl status prometheus

的输出里,方便你识别。

After#

这个字段不是“依赖谁”,而是更接近:

如果两者都要启动,那我排在谁后面。

例如:

After=network.target

意思是:

  • 这个服务应该在网络目标之后再启动

这对很多需要网络的服务都很常见。

WantsRequires#

这两个容易让新手混淆。

可以先粗略地这样理解:

  • Wants=:我希望它也一起起来,但它失败了不一定把我一起拖死
  • Requires=:我依赖它,它不行我通常也不该继续

例如:

Wants=network-online.target
After=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

意思是:

  • 正常退出不重启
  • 异常退出就尝试重启

这是我最常用的一种。

其他常见值还有:

  • no
  • always
  • on-success
  • on-abnormal

如果你托管的是一个长期运行的服务,on-failure 往往就很合适。

UserGroup#

这两个字段决定服务以谁的身份运行。

例如:

User=prometheus
Group=prometheus

为什么这很重要?

因为很多服务不应该直接用 root 跑。

如果它只是:

  • 监听一个高位端口
  • 读自己的配置
  • 写自己的数据目录

那通常更好的做法是:

  • 给它建一个专门用户
  • 只给它需要的权限

这会更安全,也更容易管理权限问题。

WorkingDirectory#

有些程序启动时依赖当前工作目录。

例如:

WorkingDirectory=/opt/myapp

它的作用就是:

  • 启动这个服务时,先进入这个目录

如果你的程序:

  • 读相对路径文件
  • 依赖当前目录

那这个字段很有帮助。

EnvironmentEnvironmentFile#

这两个字段特别实用,因为现实里很多服务都依赖环境变量。

Environment#

可以直接写单个变量,例如:

Environment=APP_ENV=production
Environment=PORT=8080

EnvironmentFile#

如果变量很多,更适合单独放到文件里:

EnvironmentFile=/etc/myapp/myapp.env

这往往比把一堆变量硬塞进 service 文件里更清楚。

这也是很多人后面会选择的方式。

ExecStartPreExecStartPost#

这两个字段分别表示:

  • 启动前先做什么
  • 启动后再做什么

例如:

ExecStartPre=/usr/bin/mkdir -p /var/lib/myapp

这在某些需要准备目录、检查配置的场景里会很好用。

第三部分:[Install]#

这一段很多新手第一次最容易忽略,但它和“开机自启”关系很大。

WantedBy#

最常见的是:

WantedBy=multi-user.target

你可以先把它理解成:

  • 这个服务应该被挂到正常多用户系统启动流程里

然后你执行:

Terminal window
systemctl enable myapp

systemd 就会根据这个字段,把它链接到对应 target 下。

换句话说:

没有 [Install],服务也能手动启动,但通常没法正常 enable

一个比较完整、适合新手参考的例子#

下面这份 service 文件,已经足够覆盖很多常见场景了:

[Unit]
Description=My Python App
After=network.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/usr/bin/python3 /opt/myapp/app.py
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target

这个例子大概表达的是:

  • 这是一个 Python 服务
  • 需要网络起来后再启动
  • 用专门用户跑
  • 工作目录在 /opt/myapp
  • 环境变量从单独文件读
  • 真正执行的是 python3 app.py
  • 异常退出后自动重启
  • 可以加入开机启动流程

写完 service 文件后,要做什么#

很多新手第一次写完 unit 文件之后,会直接 systemctl start,然后发现 systemd 像没看见一样。

这是因为:

写完或者改完 unit 文件后,通常要先 reload。

标准流程一般是:

Terminal window
systemctl daemon-reload
systemctl enable --now myapp
systemctl status myapp --no-pager

如果只是改了服务内容但不想立即 enable,也至少要:

Terminal window
systemctl daemon-reload
systemctl restart myapp

最常见的几个命令#

查看状态#

Terminal window
systemctl status myapp

启动#

Terminal window
systemctl start myapp

停止#

Terminal window
systemctl stop myapp

重启#

Terminal window
systemctl restart myapp

开机自启#

Terminal window
systemctl enable myapp

取消开机自启#

Terminal window
systemctl disable myapp

重载 systemd 配置#

Terminal window
systemctl daemon-reload

排错时最有用的两个地方#

1. systemctl status#

这是你第一眼最该看的。

它通常会告诉你:

  • 服务有没有起来
  • 最近退出码是什么
  • 最近几条日志是什么

2. journalctl#

如果 status 不够,就去看日志:

Terminal window
journalctl -u myapp -n 100 --no-pager

这个命令对排错非常有用。

如果你发现“命令行能跑,systemd 里不能跑”,十有八九要从这里开始查。

新手最常见的坑#

1)ExecStart 写了相对路径#

尽量别写:

ExecStart=python app.py

更稳的是:

ExecStart=/usr/bin/python3 /opt/myapp/app.py

2)依赖 shell 环境#

你终端里能跑,不代表 systemd 里也能跑。

比如:

  • .bashrc 里的 PATH
  • alias
  • fnm/nvm/mise
  • 代理变量

这些都可能在 systemd 环境里不存在。

3)忘记 daemon-reload#

这是特别常见的低级坑。

4)权限不对#

如果你指定了 User=myapp,那就要确保:

  • 程序文件它能读
  • 工作目录它能进
  • 日志目录或数据目录它能写

如果你想慢慢学,不用一次记住全部#

对新手来说,我建议先记住下面这几个字段:

  • Description
  • After
  • Type
  • User
  • WorkingDirectory
  • EnvironmentFile
  • ExecStart
  • Restart
  • WantedBy

只要这几个理解清楚,大多数基础 service 文件你都已经能写出来了。

写在最后#

systemd 配置文件一开始看起来像是“很多固定格式的死规则”,但真正写过几次之后,你会发现它本质上只是在回答几个问题:

  • 这是什么服务
  • 什么时候启动
  • 用谁来跑
  • 具体跑什么命令
  • 出问题后怎么办

你只要按这个思路去看,每个字段都会顺很多。

如果你刚开始接触 Linux 服务托管,我会建议你下一步就自己找一个小程序,给它手写一份 .service 文件,然后跑一遍:

  • daemon-reload
  • enable --now
  • status
  • journalctl

只要你亲手把一个程序交给 systemd 管起来,这套东西就不再只是概念了。

systemd 配置文件怎么写:从 service 文件结构到常见字段的详细入门
https://blog.yangyus8.top/posts/20260413-01/
作者
杨与S8
发布于
2026-04-13
许可协议
CC BY-NC-SA 4.0