起因
部署一个 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
简单 + 工业级稳定。
踩过的坑
-
改了 service file 没 daemon-reload:systemctl 用老版本。
daemon-reload必须。 -
WorkingDirectory 不存在:service 启不来报错 213。
journalctl -u myapp看具体原因。 -
env 变量空格转义:
Environment="K=v with space"双引号必须。 -
Type=forking 错用:应用 fork 后 systemd 跟丢主进程。多数 web
server 用Type=simple/notify。 -
PID 文件错:traditional daemon 写 PID 文件,systemd 不靠它。
PIDFile=配置是给 forking type 用。
登录后参与评论。