用 systemd 托管 Python Web 应用:Gunicorn + Flask 开机自启与崩溃自动重启实战
一、为什么不要再用 nohup 跑服务
刚买云服务器时,最常见的部署方式是这样一行命令:
nohup python 入口文件 > app.log 2>&1 &
它能跑,但问题很多:服务器重启后服务不会自动启动;进程因为异常退出后没人重新拉起;日志文件越写越大,没有轮转;想重启服务还得先 ps 找进程号再 kill。生产环境里,这些都是隐患。
现在主流 Linux 发行版(Ubuntu、Debian、CentOS、EulerOS、openEuler 等)都使用 systemd 作为初始化系统。只要写一个几十行的 unit 文件,就能让应用享受到和 nginx、sshd 一样的待遇:开机自启、失败重启、统一日志、权限隔离。
二、准备一个示例应用
假设项目放在 /opt/myapp,先创建独立的运行用户,避免用 root 跑应用:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
sudo mkdir -p /opt/myapp
sudo chown -R myapp:myapp /opt/myapp
创建虚拟环境并安装依赖:
cd /opt/myapp
sudo -u myapp python3 -m venv venv
sudo -u myapp ./venv/bin/pip install flask gunicorn
写一个最简单的 Flask 应用(项目目录下的入口文件 app):
from flask import Flask, jsonify
import os, socket
app = Flask(__name__)
@app.route("/")
def index():
return jsonify(status="ok", host=socket.gethostname(), pid=os.getpid())
@app.route("/health")
def health():
return "healthy", 200
先手动验证 Gunicorn 能正常启动:
sudo -u myapp ./venv/bin/gunicorn -w 2 -b 127.0.0.1:8000 入口文件:app
另开一个终端执行 curl 访问本机 8000 端口,返回 JSON 说明应用没问题,按 Ctrl+C 停止。
三、编写 Gunicorn 配置文件
把参数写进配置文件比堆在命令行里更清晰。创建 /opt/myapp/gunicorn.conf(Gunicorn 配置文件):
import multiprocessing
bind = "127.0.0.1:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
timeout = 30
graceful_timeout = 30
keepalive = 5
accesslog = "-" # 输出到标准输出,交给 journald 收集
errorlog = "-"
loglevel = "info"
注意 accesslog 和 errorlog 都设置为 “-”,日志会写到标准输出,systemd 会自动收进 journald,后面统一用 journalctl 查看,不用再自己管理日志文件。小内存的云服务器(比如 2 核 2G)可以把 workers 手动设为 3 左右,避免内存吃紧。
四、编写 systemd unit 文件
创建 /etc/systemd/system/myapp.service:
[Unit]
Description=MyApp Flask service (Gunicorn)
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
Environment="PYTHONUNBUFFERED=1"
EnvironmentFile=-/opt/myapp/.env
ExecStart=/opt/myapp/venv/bin/gunicorn -c /opt/myapp/gunicorn.conf 入口文件:app
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=30
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=5
MemoryMax=512M
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true
[Install]
WantedBy=multi-user.target
几个关键配置说明:
- Type=notify:Gunicorn 20 以上版本支持 systemd 通知,主进程准备就绪后才算启动成功。如果你的版本不支持,改成 Type=simple 即可。
- EnvironmentFile:数据库密码、密钥等敏感配置放在 .env 文件里,前面的减号表示文件不存在也不报错。记得 chmod 600 这个文件。
- ExecReload:给 Gunicorn 主进程发 HUP 信号,会平滑重启所有 worker,正在处理的请求不会被中断,适合更新代码后使用。
- Restart=on-failure 与 RestartSec=5:进程异常退出 5 秒后自动拉起;StartLimit 两项表示 60 秒内最多重启 5 次,防止程序有致命 bug 时无限重启刷屏。
- MemoryMax:限制服务最多使用 512M 内存,防止内存泄漏把整台机器拖死。
- NoNewPrivileges、ProtectSystem、PrivateTmp:简单的安全加固,限制进程提权、把系统目录设为只读、使用独立的临时目录。
五、启动并设置开机自启
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
看到 active (running) 就说明成功了。常用管理命令:
sudo systemctl restart myapp # 重启
sudo systemctl reload myapp # 平滑重载(更新代码后推荐)
sudo systemctl stop myapp # 停止
systemctl is-enabled myapp # 查看是否开机自启
六、用 journalctl 查看日志
journalctl -u myapp -f # 实时跟踪日志
journalctl -u myapp --since "1 hour ago" # 最近一小时
journalctl -u myapp -p err # 只看错误级别
journalctl -u myapp -n 100 --no-pager # 最近 100 行
journald 默认会自动轮转。如果担心日志占满磁盘,可以编辑 journald 配置文件,设置 SystemMaxUse=500M,然后执行 sudo systemctl restart systemd-journald。
七、验证崩溃自动重启
我们来模拟一次进程被意外杀死:
sudo systemctl show -p MainPID myapp
sudo kill -9 <上一步得到的 PID>
sleep 6
systemctl status myapp
可以看到服务已经被重新拉起,PID 变了,日志里也会记录一条 “Scheduled restart job”。再执行 sudo reboot 重启服务器,登录后 curl 一下,服务同样已经自动运行。
八、配合 Nginx 对外提供服务
Gunicorn 只监听 127.0.0.1,对外由 Nginx 反向代理,在 Nginx 站点配置里加入:
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
这样应用端口不直接暴露在公网,安全组里只需放行 80 和 443。
九、一键更新部署脚本
把更新流程写成脚本 /opt/myapp/deploy,以后发布只要运行一次:
#!/usr/bin/env bash
set -euo pipefail
cd /opt/myapp
sudo -u myapp git pull --ff-only
sudo -u myapp ./venv/bin/pip install -r requirements.txt
sudo systemctl reload myapp
sleep 2
curl -fsS http://127.0.0.1:8000/health && echo " 部署成功"
十、常见问题排查
- status 显示 code=exited, status=203/EXEC:ExecStart 路径写错或文件没有执行权限,检查虚拟环境路径是否正确。
- status=217/USER:User 指定的用户不存在,先用 useradd 创建。
- Permission denied:运行用户对项目目录没有权限,执行 chown -R myapp:myapp /opt/myapp。
- 修改了 service 文件没生效:每次修改后都要先执行 sudo systemctl daemon-reload。
- 启动超时:Type=notify 但 Gunicorn 版本过旧不发送通知,升级 Gunicorn 或改为 Type=simple。
- 服务反复重启后停止:触发了 StartLimitBurst,先用 journalctl 找到根本错误,修复后执行 sudo systemctl reset-failed myapp 再启动。
总结
用 systemd 托管 Python 应用几乎没有额外成本:一个 unit 文件、几条命令,就能获得开机自启、崩溃自愈、统一日志、平滑重载和资源限制这些生产级能力。这套方法同样适用于 FastAPI(把 Gunicorn 的 worker_class 换成 uvicorn.workers.UvicornWorker)、Django、Celery worker、定时脚本等各种后台进程。下次再部署服务时,不妨和 nohup 说再见。
- 点赞
- 收藏
- 关注作者
评论(0)