首页/系统/Linux 运维命令速查/编写一个生产级 .service 单元
systemd 与服务42 条

编写一个生产级 .service 单元

自建服务落成 systemd 单元的完整要点:ExecStart 绝对路径、User 降权、Restart 策略、日志与依赖、环境变量。

Linux 运维命令速查系统更新于 2026-10-13

单元文件的三个段落

一个 .service 文件由三段组成:[Unit] 描述单元自身的元信息与依赖关系,[Service] 定义怎么启动、以谁的身份运行、失败后如何处理,[Install] 决定 enable 时该被哪个启动目标拉起。自定义单元放在 /etc/systemd/system 下优先级高于发行版自带的 /usr/lib/systemd/system。写好这三段,服务就有了可管理、可自启、可观测的托管方式。

基本信息

适用平台Linux(systemd 发行版)
文件位置/etc/systemd/system/ 用于自定义,/usr/lib/systemd/system/ 是发行版自带的,同名时前者优先
Type 取值simple 前台进程(默认)、exec 等主程序执行后再算启动成功、forking 传统后台守护进程、oneshot 一次性任务、notify 支持 sd_notify 的应用、idle
常用字段ExecStart、ExecReload、ExecStop、User/Group、WorkingDirectory、Environment/EnvironmentFile、Restart/RestartSec、LimitNOFILE、After/Wants/Requires
权限与生效写单元文件需要 root;改动后必须 systemctl daemon-reload,再 restart 才生效

典型使用场景

  • 托管自研程序:把二进制或脚本注册成服务,获得开机自启与崩溃重启能力
  • 降权运行:用 User 与 Group 让服务以专用账号运行,避免以 root 暴露面
  • 资源与信号管理:用 LimitNOFILE 提高文件描述符上限,用 ExecReload 定义平滑重载动作
  • 启动依赖:用 After 与 Wants 保证数据库等前置服务先就绪
  • 环境注入:通过 EnvironmentFile 从文件读配置,避免把密钥写进单元文件
  • 覆盖而不改原文件:用 drop-in 目录追加或覆盖个别字段

生产级模板

生产级模板
[Unit]
Description=My App
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/opt/myapp
Environment=JAVA_HOME=/usr/lib/jvm/java-17
EnvironmentFile=-/etc/myapp/env
ExecStart=/opt/myapp/bin/start --config /etc/myapp/app.conf
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target

启用与覆盖

启用与覆盖
systemctl daemon-reload
systemctl enable --now myapp
systemctl status myapp --no-pager
systemctl cat myapp                      # 确认最终生效的定义
mkdir -p /etc/systemd/system/myapp.service.d
cat > /etc/systemd/system/myapp.service.d/override.conf <<'EOF'
[Service]
Environment=APP_ENV=prod
EOF
systemctl daemon-reload && systemctl restart myapp

关键字段对照

字段作用注意
ExecStart启动命令,必须是绝对路径不支持管道与重定向,需要 shell 就用 bash -c 包一层
ExecReload定义 reload 动作缺失时 systemctl reload 会失败
Restart / RestartSec异常退出后是否重启及间隔on-failure 比 always 更适合排查期
User / Group以哪个账号运行服务账号建议用 nologin shell
After / Wants启动顺序与弱依赖Wants 失败不会阻止本单元启动
EnvironmentFile从文件读环境变量路径前加减号表示文件可缺失
ExecStart 里的 $MAINPID 之类变量由 systemd 展开,而 Environment 里写的 $VAR 不会被 shell 展开——单元文件不是 shell 脚本。需要在命令里做变量拼接时,写成 ExecStart=/bin/bash -c '...' 并注意引号嵌套,或把值放进 EnvironmentFile 由程序自己读取。Type=notify 与 Type=exec 的差别也值得注意:前者要求应用主动上报就绪,systemd 才认为启动完成;后者只要主程序成功执行即算成功,对支持 sd_notify 的应用用 notify 可以避免依赖方在服务真正就绪前就开始调用。
加固字段值得提前加上:NoNewPrivileges=true 禁止进程再获取新特权,PrivateTmp=true 隔离 /tmp,ProtectSystem=strict 与 ReadWritePaths 组合可以把文件系统设为只读并只放行必要目录。这些设置在服务出问题时能显著缩小影响面。另外不要直接修改 /usr/lib/systemd/system 下的发行版自带单元,升级会覆盖你的改动;自定义应放在 /etc/systemd/system 下,或用 myapp.service.d/override.conf 这类 drop-in 片段追加字段。
先确认应用属于哪种启动方式再选 Type:把后台化的老程序配成 Type=simple,systemd 会因为父进程退出而认为服务已结束;反过来把前台进程配成 forking,则会一直等到超时。类型选错的典型表现是「启动成功却立刻变成 inactive」,遇到这种现象先核对 Type 与程序的实际启动行为,再去看应用日志。
服务账号与文件权限要一起设计:专用账号、不可登录的 shell、只开放必要目录,再配合单元里的工作目录与资源限制,服务才能在权限最小化的前提下稳定运行。单元里声明的限制只作用于该服务,比改全局参数更安全,也不会波及同机上的其他组件。

易错点

  • ExecStart 使用管道、重定向或 shell 语法会直接启动失败,需要 shell 功能时必须用 /bin/bash -c 包一层并注意引号
  • 把 Environment 当 shell 变量使用,$VAR 不会被展开,导致配置注入不到应用;应改用 EnvironmentFile 或在命令中让 shell 处理
  • 改了单元文件却忘记 systemctl daemon-reload 与 restart,服务仍在用旧定义运行
  • WorkingDirectory 指向不存在的目录会以 203 之类的执行错误失败,排查时应先确认路径与权限
  • 不设置 User 就让服务以 root 运行,放大漏洞影响;服务账号建议用专用账号配合 nologin shell
  • 把生产参数直接写进发行版自带的单元文件,系统升级后改动被覆盖;自定义内容应放在 /etc/systemd/system 或 drop-in 片段中
  • 用 root 跑服务再靠禁用某些命令补救:权限边界已经全部打开,一次注入就可能接管整机,正确做法是让服务账号只拥有它需要的最小权限

数据来源与参考

  • man 5 systemd.service
  • man 5 systemd.exec(User/Environment 等)

相关工具

​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌