Python 项目部署到服务器,很多人第一次踩的坑是:在终端里 python app.py 跑得好好的,一放到服务器上就出各种问题——依赖版本不对、系统 Python 被弄乱、pip install 报权限错误、Flask/Django 自带的开发服务器一上量就崩。这些问题的根子在于:Python 的开发环境和生产环境是两套东西,需要不同的组织方式。
这篇文章讲的就是把 Python 应用规范地放到 Linux 服务器上运行。顺序是按实际部署流程来的:先搞清楚系统 Python 和虚拟环境的关系(这是最容易伤到系统的地方),再讲依赖安装和国内源配置;然后重点讲 Gunicorn——worker 数怎么定、绑定 Unix socket 还是 TCP、超时参数为什么重要;接着是 Nginx 反向代理和静态文件;用 systemd 把进程托管好;最后是更新流程。
命令给 RHEL/CentOS/Rocky 与 Debian/Ubuntu 两套。涉及覆盖系统 Python、删除目录的操作会标风险提示,因为这些操作做错了可能让整台机器上的其他程序跟着挂掉。
一、Python 部署的三个基本约定
在动手之前,有三条约定需要先建立,它们决定了后面所有操作的做法。
第一,绝不要用系统自带的 Python 装项目依赖。现代 Linux 发行版里,yum、dnf、apt 这些包管理工具本身就是用 Python 写的,它们依赖系统 Python 的特定版本和特定包。如果你用系统 Python 的 pip 装了某个库的新版本,很可能把包管理工具搞坏,症状是 yum 报一堆 ImportError 或者 ModuleNotFoundError,那时候修起来非常麻烦。所以项目依赖必须装在虚拟环境里。
第二,不要用开发服务器对外提供服务。Flask 的 app.run()、Django 的 runserver 都是开发用的,它们的定位是"单进程、能调试、性能不重要"。生产环境必须用 Gunicorn、uWSGI 这类 WSGI 服务器,或者 ASGI 场景下用 Uvicorn、Hypercorn。
第三,配置和代码分离。数据库地址、密钥这些不要写死在代码里,用环境变量或者外部配置文件。这样同一份代码能部署到不同环境,也不会把密码泄进仓库。
把这三条守住,Python 部署的稳定性就有了基本盘。
二、系统 Python 与虚拟环境
先确认服务器上有什么:
# 看系统 Python 版本,不要动它
python3 --version
which python3
# 看系统里有哪些 Python
ls -l /usr/bin/python*
# RHEL 系看看有没有额外版本可以装
dnf list available "python3*"
创建虚拟环境需要 venv 模块。有些最小化安装的系统里它是单独的包:
# RHEL / Rocky / AlmaLinux
sudo dnf install -y python3 python3-pip python3-devel
# Debian / Ubuntu 里 venv 和 pip 是单独的包,新手最容易漏
sudo apt update
sudo apt install -y python3 python3-venv python3-pip python3-dev
# 验证 venv 可用
python3 -m venv --help > /dev/null && echo "venv 可用"
python3-devel(Debian 系叫 python3-dev)看着不起眼,但装原生扩展(比如 psycopg2、lxml、cryptography)时必须有它,否则会报找不到 Python.h。
用 venv 建立项目环境
# 目录约定
sudo mkdir -p /opt/app
sudo useradd -r -s /sbin/nologin appuser
sudo chown -R appuser:appuser /opt/app
# 用运行用户创建虚拟环境,避免权限混乱
sudo -u appuser python3 -m venv /opt/app/venv
# 激活(日常手工操作时)
source /opt/app/venv/bin/activate
which python # 确认指向 /opt/app/venv/bin/python
这里有个细节很重要:虚拟环境不要用 root 创建,而应该用最终运行应用的那个用户创建。否则 venv 目录属主是 root,运行用户没有写权限,pip install 会失败,或者需要一直用 sudo,把权限搞得一团糟。
虚拟环境的关键特点是隔离:它有自己的 python 可执行文件、自己的 site-packages、自己的 pip。激活后,pip install 装的东西只在这个环境里可见,对系统 Python 毫无影响。这也是它的价值所在。
venv 有一个容易误解的点:它不包含 Python 解释器本身,只是通过软链接指回系统的 Python。这意味着虚拟环境不能跨 Python 主版本迁移,也就是说你不能把一个用 Python 3.9 建的 venv 拿到只有 3.11 的机器上用,必须重建。
| 方案 | 隔离程度 | 适合场景 | 注意 |
|---|---|---|---|
| 系统 Python | 无 | 系统工具自身 | 绝不要装项目依赖 |
| venv | 环境隔离 | 绝大多数项目 | 标准库自带,首选 |
| virtualenv | 环境隔离 | 需要兼容老 Python 2 | 第三方实现 |
| conda | 环境 + 二进制依赖 | 科学计算、跨语言依赖 | 体积大,服务器上偏重 |
| 容器 | 完全隔离 | 云原生、多环境一致性 | 需要 Docker 或 K8s |
给中小团队的结论:用 venv 就够了,它是标准库自带,不需要额外安装,隔离性完全满足常规 Web 应用的需要。
三、依赖安装与国内源
依赖清单有两种组织方式:requirements.txt 和 pyproject.toml(配合 uv、poetry、pip-tools 等工具)。前者简单直接,后者更规范但需要额外的工具链。这里以 requirements.txt 为主线,因为它最通用。
# 在开发机上导出精确版本(含依赖的依赖)
pip freeze > requirements.txt
# 更好的做法:手工维护直接依赖,用 pip-tools 生成锁文件
# pip install pip-tools
# pip-compile requirements.in -o requirements.txt
生产环境安装依赖:
# 用虚拟环境里的 pip 绝对路径,不依赖激活状态
/opt/app/venv/bin/pip install -r requirements.txt
# 加国内源加速(清华源)
/opt/app/venv/bin/pip install -r requirements.txt \
-i https://pypi.tuna.tsinghua.edu.cn/simple
# 永久配置,写入 ~/.pip/pip.conf 或 /etc/pip.conf
# /etc/pip.conf (或虚拟环境所在用户的 ~/.pip/pip.conf)
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
trusted-host = pypi.tuna.tsinghua.edu.cn
timeout = 60
几个实践要点。第一,生产环境的依赖应该有精确版本号,用 == 而不是 >=,避免某天自动升级到不兼容的新版本导致服务挂掉。第二,把 requirements.txt 当成代码的一部分维护,修改它要经过评审。第三,部署时不要用 pip install -U,那会强制升级,破坏可复现性。
如果要装的项目包含大量包、容易和系统环境冲突,可以考虑用容器把整个运行环境打包,但这是另一个话题了。
风险提示:pip install 时不要加 --break-system-packages 去强行往系统 Python 里装东西,也不要 sudo pip install。这两个操作都可能破坏发行版的包管理工具。如果遇到 externally-managed-environment 的提示,正确的回应是建一个 venv,而不是绕过它。
四、Gunicorn 的核心配置
Gunicorn 是 Python 生态里最常用的 WSGI 服务器。架构很简单:一个 master 进程负责管理,多个 worker 进程负责处理请求。master 不处理请求,只做监控、重启、信号转发。
首先把它装进虚拟环境:
/opt/app/venv/bin/pip install gunicorn
worker 数怎么定
这是被问得最多的问题。经验公式是:
worker 数 = 2 × CPU 核心数 + 1
这个公式的来源是:假设每个请求在处理期间有一定比例的时间在等 IO(数据库、缓存、外部接口),那么起比核心数多一倍的 worker,能让 CPU 在等待期间不闲置。它是经验值,不是定律。具体该定多少,要看应用类型:
- IO 密集(大多数 Web 应用查数据库):
2 × 核心 + 1是合适的起点。 - CPU 密集(大量计算、图像处理):worker 数接近核心数更合适,太多了反而因为进程切换损失性能。
- 内存受限:worker 数受内存约束,每个 worker 都会加载一份应用和依赖,内存不够时宁可少起。经验上每个 Django/Flask worker 常驻几十到两百 MB 不等。
另外要注意 worker 类型的选择。同步 worker(默认)适合常规场景,每个 worker 一次处理一个请求;异步 worker(gevent、eventlet)能在单进程内处理大量并发连接,适合高并发长连接场景,但要求代码是协程安全的,而且某些阻塞型的 C 扩展库会导致问题。先用默认的同步 worker,确认有并发瓶颈再考虑换。
绑定方式:Unix socket 与 TCP
Gunicorn 可以绑到 TCP 端口,也可以绑到 Unix socket。这个选择值得单独讲。
# 绑定 TCP
gunicorn -b 127.0.0.1:8000 myproject.wsgi:application
# 绑定 Unix socket,文件权限交给 Nginx 用户(通常是 www-data 或 nginx)
gunicorn -b unix:/run/gunicorn/myapp.sock --umask 007 myproject.wsgi:application
Unix socket 的优势:不经过 TCP 协议栈,少一层开销,性能略好;不占用端口,不会有端口冲突;文件权限天然限制访问范围。缺点是调试时要记住路径、某些工具不支持、跨主机部署用不了。Nginx 和 Gunicorn 在同一台机器上时,Unix socket 是更好的选择。
TCP 的优势是通用、好调试(可以直接 curl 127.0.0.1:8000 验证应用本身是否正常)。如果 Nginx 和后端不在同一台机器,只能用 TCP。
| 对比项 | Unix socket | TCP 端口 |
|---|---|---|
| 性能 | 略好(少一层协议栈) | 略差 |
| 端口占用 | 不占用 | 占用 |
| 权限控制 | 文件权限 | 防火墙 + 监听地址 |
| 跨主机 | 不支持 | 支持 |
| 调试便利 | 一般,需 curl --unix-socket | 方便 |
| 推荐场景 | 同机 Nginx + 应用 | 分离部署 |
超时参数与常见配置
Gunicorn 有几个参数如果不去调,会在特定业务下变成定时炸弹。
--timeout 默认 30 秒,指的是 worker 在处理一个请求时静默超过这个时间就会被 master 杀掉重启。这里的"静默"是指没有产生任何输出,长时间的计算任务很容易触发它。如果你的接口有需要跑一两分钟的导出任务,必须调大这个值,或者更好的做法是把这类任务改成异步任务(Celery、RQ 之类)丢到队列里跑。
--graceful-timeout 默认 30 秒,是优雅重启时等待 worker 处理完当前请求的时间。重新加载代码时用得到。
--max-requests 配合 --max-requests-jitter 可以让 worker 在处理一定数量的请求后自动重启,用于缓解潜在的内存泄漏。取值可以设在 1000 到 5000 之间,加上抖动避免所有 worker 同时重启。
--worker-tmp-dir 在某些容器或只读文件系统里需要显式指定到 /dev/shm 之类的可写目录,否则启动会报错。
一个完整的启动命令长这样:
/opt/app/venv/bin/gunicorn \
--workers 5 \
--worker-class sync \
--bind unix:/run/gunicorn/myapp.sock \
--umask 007 \
--timeout 60 \
--graceful-timeout 30 \
--max-requests 2000 \
--max-requests-jitter 200 \
--access-logfile /var/log/app/access.log \
--error-logfile /var/log/app/error.log \
--log-level info \
--capture-output \
--preload \
myproject.wsgi:application
--preload 表示 master 在 fork worker 之前先加载应用,好处是省内存(代码段共享)和启动更快,但如果你在应用加载阶段就建立了数据库连接或做了其他"每个进程都要独立来做"的事情,preload 会导致连接在 fork 后共享,出现奇怪的问题。拿不准时先不加。
myproject.wsgi:application 这个写法要对照你的项目结构:Django 项目里 wsgi.py 通常在工程目录下,里面有一个叫 application 的变量;Flask 项目常用 app:app(模块名:变量名)。
五、Nginx 反向代理与静态文件
Python 应用的一个特点是"不擅长伺候静态文件"。Django 的静态文件、用户上传的图片视频,都应该交给 Nginx 直接返回,不要让请求穿过 Gunicorn。
upstream django_app {
server unix:/run/gunicorn/myapp.sock;
}
server {
listen 443 ssl http2;
server_name www.example.com;
ssl_certificate /etc/nginx/ssl/site.crt;
ssl_certificate_key /etc/nginx/ssl/site.key;
client_max_body_size 50m;
# Django 的静态文件,由 collectstatic 收集到统一目录
location /static/ {
alias /opt/app/static/;
expires 30d;
access_log off;
add_header Cache-Control "public";
}
# 用户上传的媒体文件
location /media/ {
alias /opt/app/media/;
expires 7d;
}
location / {
proxy_pass http://django_app;
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;
proxy_redirect off;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
两个容易出问题的配置。一是 proxy_read_timeout 必须大于等于 Gunicorn 的 --timeout,否则 Nginx 会在后端还在处理时就返回 504,现象是"日志里后端明明处理成功了,用户却看到 504 超时"。反过来,如果 Nginx 超时设得远大于 Gunicorn 超时,用户要等更久才能拿到错误,体验也不好。让两者匹配(比如都是 60 秒)是最省心的。
二是 proxy_set_header X-Forwarded-Proto $scheme 这几行必须传。Django 里要靠它判断请求是不是 HTTPS(用于生成绝对 URL、cookie 的 secure 标志、CSRF 校验)。如果配了 Nginx 做 SSL 终结但没传这个头,Django 会认为自己在跑 HTTP,重定向之后可能陷入循环,或者 CSRF 校验失败。Django 侧还需要设置 SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')。
Django 项目在上线前别忘了跑 collectstatic:
DJANGO_SETTINGS_MODULE=myproject.settings.prod \
/opt/app/venv/bin/python manage.py collectstatic --noinput
Flask 应用如果用 url_for('static', ...),Nginx 的 alias 要指向应用的 static 目录。
六、用 systemd 托管 Gunicorn
不要用 nohup gunicorn ... & 这种方式,理由和 Spring Boot 那篇一样。用 systemd:
# /etc/systemd/system/myapp.service
[Unit]
Description=My Python App (Gunicorn)
After=network-online.target
Wants=network-online.target
[Service]
Type=notify # Gunicorn 支持 systemd 通知;不支持时改用 simple
User=appuser
Group=appuser
WorkingDirectory=/opt/app/current
Environment="PATH=/opt/app/venv/bin:/usr/local/bin:/usr/bin:/bin"
Environment="PYTHONUNBUFFERED=1"
Environment="PYTHONDONTWRITEBYTECODE=1"
EnvironmentFile=-/etc/app/app.env
# 运行时目录,systemd 会自动创建和清理
RuntimeDirectory=gunicorn
RuntimeDirectoryMode=0755
ExecStart=/opt/app/venv/bin/gunicorn \
--workers 5 \
--bind unix:/run/gunicorn/myapp.sock \
--umask 007 \
--timeout 60 \
--access-logfile - \
--error-logfile - \
myproject.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
Restart=always
RestartSec=5
TimeoutStopSec=30
# 日志交给 journald 收集(若同时想落地到文件,可另配 StandardOutput=append:)
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
解释几个关键项。PYTHONUNBUFFERED=1 让 Python 的输出不缓冲,日志能实时出现在 journald 里,不加这一条你会看到日志延迟几分钟才出来,排查问题时非常难受。PYTHONDONTWRITEBYTECODE=1 避免生成 .pyc 文件污染目录。RuntimeDirectory=gunicorn 会在 /run/gunicorn 下创建目录,属主自动设为服务用户,这样 socket 文件的权限正好——比手工 mkdir 加 chown 可靠得多。
ExecReload 配了 HUP 信号,这样 systemctl reload myapp 就能触发 Gunicorn 的优雅重载(重新加载代码,不中断服务),升级流程会顺畅很多。
Type=notify 要求 Gunicorn 带着 systemd 支持(较新版本默认支持)。如果启动报"Failed with result 'protocol'",把它改成 Type=simple 即可。
启用并启动:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
# 查看状态和日志
sudo systemctl status myapp
sudo journalctl -u myapp -n 100 --no-pager
sudo journalctl -u myapp -f
日志
Gunicorn 的两类日志要分清:access log 记录每个请求(谁、什么时候、访问了什么、状态码、耗时),error log 记录应用错误和 Gunicorn 自身的异常。用 --access-logfile - 和 --error-logfile - 输出到标准输出,由 journald 收集,这样日志集中且自带轮转。
但 journald 有总量限制,日志量大的应用建议同时落到文件并做切割。用 logrotate:
# /etc/logrotate.d/myapp
/var/log/app/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 appuser appuser
sharedscripts
postrotate
systemctl reload myapp > /dev/null 2>&1 || true
endscript
}
# 测试配置是否正确(-d 是 dry run,不会真执行)
sudo logrotate -d /etc/logrotate.d/myapp
# 手动强制跑一次
sudo logrotate -f /etc/logrotate.d/myapp
风险提示:logrotate 的 create 权限要和实际运行用户一致,否则切割之后新日志文件属主是 root,应用写不进去,会静默丢失日志。改完务必用 -d 先验证。
七、更新与回滚
Python 项目的更新比 Node 更需要注意依赖:因为依赖装在虚拟环境里,升级时必须同步更新。推荐的做法是每次发布建一个新的虚拟环境,而不是在原环境上增量 pip install——后者会因为残留的旧包产生"本地装过某个包所以能跑,新机器上不行"这类问题。
#!/bin/bash
set -euo pipefail
APP_DIR=/opt/app
TS=$(date +%Y%m%d%H%M%S)
NEW_VENV="$APP_DIR/venvs/$TS"
# 1) 拉代码到新目录(用 git worktree 或直接 clone 到 releases)
git -C "$APP_DIR/src" fetch --all --prune
git -C "$APP_DIR/src" reset --hard origin/main
# 2) 建全新虚拟环境并装依赖
python3 -m venv "$NEW_VENV"
"$NEW_VENV/bin/pip" install --upgrade pip -q
"$NEW_VENV/bin/pip" install -r "$APP_DIR/src/requirements.txt" -q
# 3) 切换软链接(原子操作)
ln -sfn "$APP_DIR/src" "$APP_DIR/current"
ln -sfn "$NEW_VENV" "$APP_DIR/venv"
# 4) 优雅重载
systemctl reload myapp || systemctl restart myapp
# 5) 健康检查,失败回滚
sleep 3
if ! curl -fsS --max-time 5 http://127.0.0.1:8000/health > /dev/null 2>&1; then
echo "健康检查失败,回滚"
ln -sfn "$APP_DIR/releases/previous" "$APP_DIR/current"
systemctl restart myapp
exit 1
fi
# 6) 清理旧的虚拟环境,保留最近 3 个
ls -1dt "$APP_DIR"/venvs/* | tail -n +4 | xargs -r rm -rf
echo "更新完成"
风险提示:最后一步的 rm -rf 会真删目录描述符指向的内容。上线前先用 ls -1dt 单独执行一遍,确认排序和要保留的条目符合预期,再打开删除逻辑。另外,运行用户需要有创建虚拟环境和写 venvs 目录的权限,否则脚本会中途失败。
常见问题
Q:pip install 报 "Externally managed environment" 怎么办?
这是新版 Debian/Ubuntu 的保护机制,防止你污染系统 Python。正确做法是创建虚拟环境再装,不要加 --break-system-packages 绕过它。
Q:Gunicorn 启动报 "Worker failed to boot",但看不出原因?
先在终端手动用同样的命令跑一次,错误会直接打印出来。最常见的原因是 module:variable 路径写错、虚拟环境里缺依赖、或者应用加载阶段就抛异常(数据库连接失败、配置项缺失)。也可以临时加 --log-level debug 看更多信息。
Q:接口偶发 502,Nginx 日志写 "upstream prematurely closed connection"?
通常是 worker 被超时杀掉或者崩溃了。检查 Gunicorn 的 error log,看有没有 Worker timeout 的记录。如果有,说明 --timeout 太小,或者确实有请求处理得太久。另外 socket 文件权限不对也会导致连接失败,确认 Nginx 用户对 socket 有读写权限。
Q:Django 后台样式全丢了?
collectstatic 没跑,或者 Nginx 的 alias 指向的目录和 STATIC_ROOT 不一致。检查 STATIC_ROOT 的配置值,确认文件确实在那个目录下,再核对 Nginx 配置。另外 DEBUG=False 时 Django 不会自己伺服静态文件,必须靠 Nginx,这是设计如此。
Q:怎么在不重启服务的情况下更新代码?
systemctl reload myapp(配合 ExecReload 的 HUP)能触发 Gunicorn 优雅重载。但要注意它只重新加载代码,不会重新安装依赖。如果本次改动涉及依赖变化,必须重启服务并重建虚拟环境。
总结
Python 部署的纪律可以压缩成几句话:系统 Python 只用来创建虚拟环境,项目依赖一律装在 venv 里;生产环境用 Gunicorn 而不是开发服务器;配置通过环境变量注入,不写死在代码里。
Gunicorn 的调参记住三条:worker 数从 2 × 核心 + 1 起步再按实测调整;同机部署用 Unix socket 更有效率;--timeout 一定要对照业务最长处理时间来设,否则长任务会被反复杀掉。
Nginx 负责静态文件、TLS 和反向代理,proxy_read_timeout 要和 Gunicorn 的 timeout 对齐,X-Forwarded-Proto 必须传。进程用 systemd 托管,配上 PYTHONUNBUFFERED=1 保证日志实时、RuntimeDirectory 自动管理 socket 目录、ExecReload 支持优雅重载。
发布流程上,每次构建全新的虚拟环境并做软链接切换,比在旧环境上增量升级可靠得多,回滚也只是改一条链接。
企业QQ咨询




