systemd 服务编写:让自己的应用像个正经服务
写个脚本用 nohup 后台跑能活,但进程挂了不会自动拉起、重启后不会自启、日志到处都是。写一份 systemd unit 就能一次性解决。
一、unit 文件长什么样
放在 /etc/systemd/system/myapp.service:
[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 跑应用。建一个专用用户:
sudo useradd -r -s /usr/sbin/nologin myapp
sudo chown -R myapp:myapp /srv/myapp-r 表示系统用户(不能登录),安全性更好。
Environment 与 EnvironmentFile
# 直接写
Environment=NODE_ENV=production
# 从文件读(推荐,密码不进 unit 文件)
EnvironmentFile=/srv/myapp/.env.env 文件格式就是 KEY=value 一行一个,注意不能用 export、不能有引号解析:
# /srv/myapp/.env
DB_HOST=127.0.0.1
DB_PASSWORD=some-secretchmod 600 /srv/myapp/.env
chown myapp:myapp /srv/myapp/.env三、完整模板:Node 应用
[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.targetPython 应用把 ExecStart 换成:
ExecStart=/srv/venv/bin/gunicorn -w 2 -b 127.0.0.1:5000 app:app四、操作命令
# 写完先让 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 路径不对或文件没执行权限:
which node # 一定要用绝对路径
ls -l /srv/myapp/server.js
sudo -u myapp /usr/bin/node /srv/myapp/server.js # 手动跑一次看报错启动后立即退出
journalctl -u myapp -n 50 --no-pager常见原因:端口被占用、配置文件读不到、工作目录不对、环境变量缺失。
改了 unit 没生效
忘了 daemon-reload。这是最高频的错误。
sudo systemctl daemon-reload
sudo systemctl restart myapp权限问题
# 以目标用户身份测试能否读取文件
sudo -u myapp test -r /srv/myapp/.env && echo OK
namei -l /srv/myapp/.env六、定时器:替代 cron
定时备份用 timer 比 cron 好,能看到状态和日志。需要两个文件:
# /etc/systemd/system/backup.service
[Unit]
Description=Daily backup
[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.targetsudo systemctl enable --now backup.timer
systemctl list-timers | grep backup
journalctl -u backup.servicePersistent=true 的意思是:如果关机错过了执行时间,开机后补跑一次。cron 做不到这一点。
OnCalendar 的写法:
| 表达式 | 含义 |
|---|---|
*-*-* 03:00:00 | 每天 3 点 |
Mon *-*-* 09:00 | 每周一 9 点 |
*-*-01 00:00 | 每月 1 号 |
*:0/15 | 每 15 分钟 |
# 验证时间表达式写得对不对
systemd-analyze calendar "*-*-* 03:00:00"七、安全加固那几行值得留着
| 参数 | 作用 |
|---|---|
NoNewPrivileges | 进程无法提权 |
PrivateTmp | 独立的 /tmp,看不到别人的临时文件 |
ProtectSystem=strict | 整个文件系统只读 |
ProtectHome | /home 不可访问 |
ReadWritePaths | 在只读之上开白名单 |
这几行加上之后,即使应用被入侵,攻击者能碰到的范围也小得多。代价几乎为零,很划算。
