systemd 管理服务:替代 supervisord / pm2 / forever

起因

部署一个 Python / Node / Go 应用,需要:

  • 后台跑(不 attach 终端)
  • 开机自启
  • crash 自动重启
  • 集中查 log
  • restart / stop 标准命令

老办法:

  • nohup + & + 写 pid 文件(朴素 + 不重启)
  • supervisord(Python,需装)
  • pm2(Node 圈,需装)
  • forever(更老的 Node 工具)

systemd 是现代 Linux 默认(Ubuntu 16+ / RHEL 7+ / Debian 8+),
无需装第三方工具。下面写一份 service 文件就够了。

最小 service file

# /etc/systemd/system/myapp.service
[Unit]
Description=My Web App
After=network.target

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/srv/myapp
ExecStart=/srv/myapp/.venv/bin/gunicorn myapp.wsgi -b 127.0.0.1:8000 -w 4
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

启用 + 启动:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp

完事。开机自启 + 后台 + crash 自动 5 秒重启 + log 到 journald。

常用命令

systemctl status myapp           # 状态
systemctl start/stop/restart myapp
systemctl reload myapp           # 发 SIGHUP(如果应用支持 reload)
systemctl enable / disable myapp # 开机启动开关

journalctl -u myapp              # 看 log
journalctl -u myapp -f           # tail -f
journalctl -u myapp --since '1 hour ago'
journalctl -u myapp -p err       # 只看 error 级别

环境变量

[Service]
EnvironmentFile=/etc/myapp/env
Environment="DJANGO_SETTINGS_MODULE=myapp.settings.production"
Environment="PYTHONUNBUFFERED=1"

/etc/myapp/env

DATABASE_URL=postgres://...
SECRET_KEY=...

权限 600 + owned by root → secret 安全。

自动重启策略

[Service]
Restart=on-failure         # always / on-failure / on-abnormal
RestartSec=5s
StartLimitIntervalSec=300
StartLimitBurst=10         # 5 分钟内重启 > 10 次就放弃

Restart=always:包括正常退出也重启(适合 worker)。
Restart=on-failure:只 exit code ≠ 0 才重启(适合 server)。

资源限制

[Service]
MemoryMax=2G               # 超过被 OOM killed
CPUQuota=200%              # 最多用 2 核
TasksMax=512               # 子进程上限
LimitNOFILE=65535          # 文件描述符

防 runaway 进程吃光资源。容器化等价但 systemd 也能做。

安全 hardening

[Service]
NoNewPrivileges=true       # 不能 setuid
PrivateTmp=true            # 独立 /tmp
ProtectSystem=strict       # /usr / /boot 等只读
ProtectHome=true           # /home 不可见
ReadWritePaths=/var/log/myapp /var/lib/myapp
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true

systemd-analyze security myapp 评分(0-10,越低越严)。
不必全开但默认加几个稳妥。

socket activation

# /etc/systemd/system/myapp.socket
[Unit]
Description=myapp socket

[Socket]
ListenStream=127.0.0.1:8000
Accept=no

[Install]
WantedBy=sockets.target
# myapp.service
[Service]
ExecStart=/srv/myapp/server
StandardInput=socket       # 接收 socket fd

systemd 监听 port,第一个请求来才启动 service → 节省资源 + 启动期间
请求 buffered。
对 web app 较 niche,inetd 风格。

timer (取代 cron)

# /etc/systemd/system/myapp-cleanup.service
[Service]
Type=oneshot
ExecStart=/srv/myapp/.venv/bin/python /srv/myapp/cleanup.py
# /etc/systemd/system/myapp-cleanup.timer
[Timer]
OnCalendar=daily            # 每天午夜
OnCalendar=*-*-* 03:30:00   # 每天 3:30
Persistent=true             # boot 后补跑漏的

[Install]
WantedBy=timers.target
systemctl enable --now myapp-cleanup.timer
systemctl list-timers       # 看下次跑时间
journalctl -u myapp-cleanup # 历史 log

cron 优势:

  • log 集成 journald
  • 重试 / failure 处理 systemd 标准
  • random delay(多机错峰)
  • 可重用 service 单独执行

我现在新部署 0 cron,全 timer。

graceful shutdown

[Service]
ExecStart=/srv/myapp/server
TimeoutStopSec=30s          # SIGTERM 后等 30s
KillSignal=SIGTERM
ExecReload=/bin/kill -HUP $MAINPID

systemctl stop myapp → 发 SIGTERM → 应用清理 → 30s 内不退就 SIGKILL。
应用要 catch SIGTERM 完成 in-flight 请求再退。

多实例

@ template:

# /etc/systemd/system/[email protected]
[Service]
ExecStart=/srv/myapp/server --port=%i
systemctl start myapp@8001 myapp@8002 myapp@8003

3 个 instance 不同 port,共享 service 模板。

user service

不需要 sudo:

# 写到 ~/.config/systemd/user/myapp.service
systemctl --user daemon-reload
systemctl --user enable --now myapp
loginctl enable-linger $USER   # 退出 shell 后仍跑

适合:个人项目 / 不能 root 的服务器。

与 supervisord 对比

systemd supervisord pm2
默认装 ✅(modern Linux) ❌(pip install) ❌(npm install)
配置语法 INI(详细) INI(简) JS/JSON
资源限制
timer ❌(用 cron)
log journald 文件 文件
跨平台 Linux only 多平台 多平台
开机启动 要 init 配 pm2 startup

Linux 服务器 systemd 完胜(已经在那)。
非 Linux / 容器(无 systemd)→ supervisord / pm2。

docker 里要 systemd?

容器内一般 PID 1 跑应用,不需要 systemd。
特殊场景(多进程 in container)用 tini / supervisord / s6-overlay

systemd in container:复杂,不推荐。如果非要,Podman 比 Docker 友好。

journald log 持久化

默认 journald 内存 + 临时存储。重启丢。
持久化:

sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald

或者 forward 到 rsyslog / Loki:

# /etc/systemd/journald.conf
[Journal]
ForwardToSyslog=yes

真实部署 case

部署 Django + gunicorn + celery worker + celery beat:

# myapp.service (gunicorn)
[Service]
ExecStart=/srv/myapp/.venv/bin/gunicorn myapp.wsgi -b 127.0.0.1:8000

# myapp-worker.service
[Service]
ExecStart=/srv/myapp/.venv/bin/celery -A myapp worker --loglevel=info

# myapp-beat.service
[Service]
ExecStart=/srv/myapp/.venv/bin/celery -A myapp beat --loglevel=info

3 个 service,systemctl status myapp myapp-worker myapp-beat 全状态。

部署 deploy script:

git pull
.venv/bin/pip install -r requirements.txt
.venv/bin/python manage.py migrate
sudo systemctl restart myapp myapp-worker myapp-beat

简单 + 工业级稳定。

踩过的坑

  1. 改了 service file 没 daemon-reload:systemctl 用老版本。
    daemon-reload 必须。

  2. WorkingDirectory 不存在:service 启不来报错 213。
    journalctl -u myapp 看具体原因。

  3. env 变量空格转义Environment="K=v with space" 双引号必须。

  4. Type=forking 错用:应用 fork 后 systemd 跟丢主进程。多数 web
    server 用 Type=simple / notify

  5. PID 文件错:traditional daemon 写 PID 文件,systemd 不靠它。
    PIDFile= 配置是给 forking type 用。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。