systemd 服务编写:让自己的应用像个正经服务

写个脚本用 nohup 后台跑能活,但进程挂了不会自动拉起、重启后不会自启、日志到处都是。写一份 systemd unit 就能一次性解决。

一、unit 文件长什么样

放在 /etc/systemd/system/myapp.service

nginx
[Unit]
Description=My Node App
Documentation=https://example.com
After=network.target
Wants=network-online.target

[Service]
Type=simple
User=baize
Group=baize
WorkingDirectory=/srv/myapp
Environment=NODE_ENV=production
EnvironmentFile=/srv/myapp/.env
ExecStart=/usr/bin/node /srv/myapp/server.js
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

[Install]
WantedBy=multi-user.target

三段各自的作用:

作用
[Unit]描述、依赖顺序
[Service]怎么启动、以谁的身份、失败怎么办
[Install]开机自启相关

二、常用参数逐个说

Type:进程类型

适用说明
simple大多数应用(默认)ExecStart 就是主进程
forking传统守护进程主进程 fork 后退出(Nginx、Redis)
oneshot执行一次就结束配合 timer 用的备份脚本
notify支持 sd_notify 的应用启动完成会主动通知

Node / Python 应用一律用 simple

Restart:挂了怎么办

行为
no不重启(默认)
always无论什么原因退出都重启(最常用
on-failure非 0 退出码时重启
on-abnormal被信号杀死或超时才重启

Restart=always + RestartSec=5,应用崩了 5 秒后自动拉起,这是 systemd 最值钱的能力。

User / Group

不要用 root 跑应用。建一个专用用户:

bash
sudo useradd -r -s /usr/sbin/nologin myapp
sudo chown -R myapp:myapp /srv/myapp

-r 表示系统用户(不能登录),安全性更好。

Environment 与 EnvironmentFile

nginx
# 直接写
Environment=NODE_ENV=production

# 从文件读(推荐,密码不进 unit 文件)
EnvironmentFile=/srv/myapp/.env

.env 文件格式就是 KEY=value 一行一个,注意不能用 export、不能有引号解析

bash
# /srv/myapp/.env
DB_HOST=127.0.0.1
DB_PASSWORD=some-secret
bash
chmod 600 /srv/myapp/.env
chown myapp:myapp /srv/myapp/.env

三、完整模板:Node 应用

nginx
[Unit]
Description=Blog API Service
After=network.target

[Service]
Type=simple
User=myapp
WorkingDirectory=/srv/blog-api
EnvironmentFile=/srv/blog-api/.env
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=5

# 安全加固:限制能力,降低被入侵后的影响面
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/blog-api/logs

# 资源限制
MemoryMax=512M
TasksMax=100

[Install]
WantedBy=multi-user.target

Python 应用把 ExecStart 换成:

bash
ExecStart=/srv/venv/bin/gunicorn -w 2 -b 127.0.0.1:5000 app:app

四、操作命令

bash
# 写完先让 systemd 重新加载配置(改了 unit 必做)
sudo systemctl daemon-reload

sudo systemctl enable --now myapp    # 开机自启 + 立即启动
sudo systemctl start myapp
sudo systemctl stop myapp
sudo systemctl restart myapp
sudo systemctl status myapp

# 看日志
journalctl -u myapp -f
journalctl -u myapp --since "1 hour ago"
journalctl -u myapp -n 100 --no-pager

五、排错

启动失败:status=203/EXEC

多是 ExecStart 路径不对或文件没执行权限:

bash
which node          # 一定要用绝对路径
ls -l /srv/myapp/server.js
sudo -u myapp /usr/bin/node /srv/myapp/server.js   # 手动跑一次看报错

启动后立即退出

bash
journalctl -u myapp -n 50 --no-pager

常见原因:端口被占用、配置文件读不到、工作目录不对、环境变量缺失。

改了 unit 没生效

忘了 daemon-reload。这是最高频的错误。

bash
sudo systemctl daemon-reload
sudo systemctl restart myapp

权限问题

bash
# 以目标用户身份测试能否读取文件
sudo -u myapp test -r /srv/myapp/.env && echo OK
namei -l /srv/myapp/.env

六、定时器:替代 cron

定时备份用 timer 比 cron 好,能看到状态和日志。需要两个文件:

nginx
# /etc/systemd/system/backup.service
[Unit]
Description=Daily backup

[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
nginx
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target
bash
sudo systemctl enable --now backup.timer
systemctl list-timers | grep backup
journalctl -u backup.service

Persistent=true 的意思是:如果关机错过了执行时间,开机后补跑一次。cron 做不到这一点。

OnCalendar 的写法:

表达式含义
*-*-* 03:00:00每天 3 点
Mon *-*-* 09:00每周一 9 点
*-*-01 00:00每月 1 号
*:0/15每 15 分钟
bash
# 验证时间表达式写得对不对
systemd-analyze calendar "*-*-* 03:00:00"

七、安全加固那几行值得留着

参数作用
NoNewPrivileges进程无法提权
PrivateTmp独立的 /tmp,看不到别人的临时文件
ProtectSystem=strict整个文件系统只读
ProtectHome/home 不可访问
ReadWritePaths在只读之上开白名单

这几行加上之后,即使应用被入侵,攻击者能碰到的范围也小得多。代价几乎为零,很划算。