单元文件的三个段落
一个 .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、只开放必要目录,再配合单元里的工作目录与资源限制,服务才能在权限最小化的前提下稳定运行。单元里声明的限制只作用于该服务,比改全局参数更安全,也不会波及同机上的其他组件。