本地调试一切正常的 Python 项目往往在搬上 CentOS 服务器的那一刻开始现原形ModuleNotFoundError、pip 编译卡死、中文日志变问号、后台进程关掉终端就消失。这套流程我在不同规模的机器上重复过很多遍从单核 1G 内存的小型主机到 8 核 16G 的生产环境都趟过踩的坑基本集中在几个固定位置。这篇内容围绕 Linux(Centos) 部署 Python 项目这件事把从系统自带的 Python 2.7 历史包袱、编译安装解释器、虚拟环境隔离、依赖安装、Gunicorn 与 Nginx 组合到 systemd 托管进程和上线后排错这一整条链路讲清楚。不管你是第一次把自己写的 Flask 小工具放上服务器还是接手了一套老项目需要重新梳理部署方式下面的步骤和判断依据都能直接拿去用。1. 为什么 CentOS 上部署 Python 项目总卡在环境这一步1.1 系统自带的 Python 2.7 是个绕不开的历史包袱CentOS 7 默认带的是 Python 2.7而且这个解释器不是给用户用的它被 yum 深度依赖。yum 本身就是一个 Python 程序它 import 的是系统路径下的那几个库。我见过太多人上来就执行rm -rf /usr/bin/python然后软链到 Python 3结果 yum 直接崩掉连修复用的工具都装不了。这个操作在 CentOS 7 上是绝对禁区记住一条系统自带的 Python 2.7 你可以不用但不要删、不要覆盖、不要动它的软链接。正确的做法是让 Python 3 和系统的 Python 2.7 并存。你要做的所有事情——装依赖、跑虚拟环境、起服务——都用绝对路径或者环境变量指向你自己装的那份 Python 3跟系统的那份完全隔离。这样做的好处是yum、firewalld 这些系统组件的脚本不会因为你升级了 Python 而报语法错误。判断机器上有没有可用的 Python 3先跑这两条python3 -V which python3如果第二条输出的是/usr/bin/python3说明这个 Python 3 是系统包管理装的CentOS 7 上一般来自 EPEL 的 python36 包。它能用但版本通常偏旧而且python3 -m venv有时缺 ensurepip 模块。我更倾向自己编译一份放到/usr/local这样版本可控编译参数可控出问题也好定位。1.2 编译安装还是包管理安装一次说清取舍这是新手最容易纠结的地方。两种方式我都用过结论是看场景不是看哪个更高级。方式优点缺点适用场景yum/EPEL 安装快、省事、依赖自动解决版本旧路径固定缺编译模块内网测试、临时验证源码编译安装版本自由编译选项可控路径干净耗时长小机器 10 到 30 分钟要手动补依赖生产环境、需要指定版本pyenv 管理多版本切换方便需要额外装团队协作时要统一一台机器跑多个项目我的默认选择是源码编译装在/usr/local/python3然后把/usr/local/python3/bin加到 PATH 里。这么做最直接的好处是将来要升级或换版本直接改软链指向新目录就行旧版本的库和 site-packages 都还在回滚成本极低。用 yum 装的 Python 3 想换版本就得处理一堆 RPM 依赖比较麻烦。机器规格也很关键。1 核 1G 的机器编译 Python 3.11 大概要二十多分钟中间如果内存爆了会直接被 OOM Killer 干掉这时候要么临时加 swap要么换交叉编译好的包。8 核机器一般两三分钟就跑完体验完全不一样。所以正式编译之前先free -h看一眼内存心里有个数。2. 把 Python 运行环境搭稳的完整动作2.1 装 Python 3 之前必须先补的编译依赖源码编译最容易翻车的地方不是 configure而是编译到一半报一堆fatal error: xxx.h: No such file or directory。这些 .h 文件来自开发库缺了就会有一堆模块编译不出来典型后果是装完之后import ssl失败、import lzma失败然后 pip 因为连不上 HTTPS 源而报 SSL 错误——很多人以为是网络问题其实根子在编译时缺了 openssl-devel。所以第一步永远是补齐开发包这条命令我基本是闭着眼睛敲yum install -y gcc gcc-c make \ zlib-devel bzip2-devel openssl-devel ncurses-devel \ sqlite-devel readline-devel tk-devel gdbm-devel \ libffi-devel xz-devel wget这个列表里的每一项都对应 Python 的一个内置模块zlib-devel对应import zlibopenssl-devel对应ssl和hashlib里的加密算法libffi-devel对应ctypes很多第三方库用它调用 C 代码比如 cffi、cryptographyxz-devel对应lzmasqlite-devel对应sqlite3Python 自带的 sqlite 模块没它就是空的。缺哪个对应的模块就是没编译进来运行时才暴露这时候你已经装完 Python 了改起来很烦。有个细节值得提醒openssl-devel 的版本会影响 Python 能不能用较新的 TLS 协议。CentOS 7 上的 openssl 是 1.0.2 系列Python 3.10 以后的版本对 openssl 版本有要求编译时可能提示找不到符合要求的 OpenSSL。这种情况要么降低 Python 版本到 3.9要么自己再编译一份新版 OpenSSL 并指定--with-openssl。对小项目来说选 Python 3.9 或者 3.8 是最省事的路线。2.2 编译安装 Python 3 的命令链条依赖补齐之后下面是完整流程。我以 Python 3.9.18 为例装在/usr/local/python3cd /usr/local/src wget https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tgz tar -xzf Python-3.9.18.tgz cd Python-3.9.18 ./configure \ --prefix/usr/local/python3 \ --enable-optimizations \ --with-ensurepipinstall make -j$(nproc) make altinstall几个参数值得解释一下因为它们直接影响后面用起来顺不顺。--prefix决定安装目录装在/usr/local/python3而不是直接装进/usr/local是为了将来多版本共存时互不干扰卸载时直接删目录就行。--enable-optimizations会做 PGOProfile Guided Optimization让解释器跑得快一些代价是编译时间明显变长通常翻倍。小机器上如果你等不起可以去掉性能差距在大多数 Web 项目里感觉不到。--with-ensurepipinstall保证装完之后 pip 是现成的省得再单独装一遍 pip。make用-j$(nproc)并行编译这是最有效的提速方式。make altinstall而不是make install这一点非常重要altinstall不会创建python3这个软链避免覆盖系统已存在的同名命令是官方推荐的多版本安装方式。装完之后建软链并验证ln -s /usr/local/python3/bin/python3.9 /usr/local/bin/python3 ln -s /usr/local/python3/bin/pip3.9 /usr/local/bin/pip3 python3 -V pip3 -V然后用一条命令验证关键模块都在这一步能提前拦住 90% 的后续问题python3 -c import ssl, zlib, sqlite3, ctypes, lzma, readline; print(ssl.OPENSSL_VERSION)如果这条命令没报错并且打印出了 OpenSSL 版本说明编译质量过关。如果哪个模块报 ModuleNotFoundError回去补对应的 devel 包再重新编译别想着绕过后面一定会在某个依赖上炸掉。2.3 venv 虚拟环境与 pip 源的配置细节解释器装好了但项目不应该直接装在全局环境里。全局装依赖的后果是两个项目要同一个库的不同版本直接冲突而且以后想清理根本没头绪。虚拟环境就是给每个项目一个独立的site-packages。mkdir -p /data/www/myproject cd /data/www/myproject python3 -m venv venv source venv/bin/activate激活之后命令行前面会出现(venv)提示符。这时候which python应该指向/data/www/myproject/venv/bin/python如果不是说明激活没生效检查是不是在正确的目录下执行。关于 pip 源国内环境直接连官方源经常超时或者慢得离谱。设置全局源的方式是写配置文件比每次加-i参数省事mkdir -p ~/.pip cat ~/.pip/pip.conf EOF [global] index-url https://pypi.tuna.tsinghua.edu.cn/simple trusted-host pypi.tuna.tsinghua.edu.cn timeout 120 EOFtrusted-host这一行的作用是跳过 HTTPS 证书校验某些老版本的 pip 加上某些源不写这个会报证书错误。这不是最优安全实践但在内网或者证书链不全的环境里确实省事你自己权衡。还有一个容易忽略的点如果机器跑的是 Python 3.9很多时候需要顺便升级一下 venv 里的 pip因为 Python 自带的老 pip 在处理某些包的元数据时会报错python -m pip install --upgrade pip升级 pip 之后再装项目依赖能避开不少莫名其妙的解析错误。3. 代码上服务器、依赖安装与目录规划3.1 上传代码与解压乱码的处理代码上传最常见的方式是 scp 或者 rsync。scp 简单直接scp -r ./myproject root192.168.1.100:/data/www/但如果是持续更新的项目rsync 更合适它能增量同步只传改动过的文件还能排除掉venv、__pycache__、.git这些不该传过去的目录rsync -avz --exclude venv --exclude __pycache__ --exclude .git \ ./myproject/ root192.168.1.100:/data/www/myproject/注意源路径后面的斜杠./myproject/和./myproject在 rsync 里语义不同前者是目录里的内容后者是目录本身。加错了会导致目标目录结构多一层嵌套这是很常见的低级错误。如果你拿到的代码是 zip 压缩包解压时中文文件名变成乱码原因通常是压缩包在 Windows 下用 GBK 编码存储文件名而 Linux 默认按 UTF-8 解释。处理方式是先用unzip -l看一眼文件名如果是乱码就指定编码unzip -O gbk package.zip如果 unzip 版本太老不支持-O参数可以用 Python 处理python3 -c import zipfile z zipfile.ZipFile(package.zip) for i in z.infolist(): i.filename i.filename.encode(cp437).decode(gbk) z.extract(i, .) tar.gz 包一般没这个问题因为 tar 不做编码转换原样存原样取。上传完之后记得改属主。如果服务最终以非 root 用户运行目录权限要提前规划好否则后面 systemd 起服务时会报 Permission deniedchown -R www:www /data/www/myproject chmod -R 755 /data/www/myproject3.2 requirements.txt 依赖安装时的编译陷阱依赖安装这一步最耗时的不是下载是编译。像psycopg2、mysqlclient、lxml、pillow、cryptography这些包如果官方没有提供对应平台和 Python 版本的预编译 wheelpip 就会拉源码在本地编译这时候要装一堆系统库编译失败的报错信息还特别长容易看晕。我的经验是先跑一次看它到底在编译什么pip install -r requirements.txt如果报错里出现pg_config not found那是 psycopg2 需要 PostgreSQL 的开发包报mysql_config not found是 mysqlclient 需要 MySQL 的开发包报fatal error: libxml/xmlversion.h是 lxml 需要 libxml2 的开发包。对应的补齐命令yum install -y postgresql-devel mysql-devel libxml2-devel libxslt-devel或者用纯 Python 的替代包来规避编译psycopg2-binary替代psycopg2mysqlclient换成pymysql。代价是性能略低换来的是部署省心中小项目完全够用。这是一个典型的工程取舍不是技术先进性问题。另一个大坑是依赖版本不锁。requirements.txt里如果只写flask今天装是 3.0半年后重装可能就变成 3.1 了中间任何不兼容改动都会让你怀疑人生。规范做法是锁死版本用pip freeze导出pip freeze requirements.txt如果想连子依赖和哈希都锁住可以用 pip-tools 从requirements.in生成带哈希的requirements.txt这个在团队协作里更稳。小项目用 freeze 就够了。3.3 目录结构与权限的规划思路一个经得起折腾的目录结构大概长这样/data/www/myproject/ ├── venv/ 虚拟环境 ├── app/ 业务代码 ├── static/ 静态文件 ├── logs/ 日志目录 ├── .env 环境变量 ├── wsgi.py WSGI 入口 └── requirements.txt把logs独立出来是有原因的日志文件会不断增长独立目录方便做按目录的清理和监控也方便挂载单独的数据盘。生产环境千万别把日志写在项目根目录时间一长目录乱得没法看。权限上有个原则值得说清楚能用普通用户跑就别用 root 跑。Web 服务以 root 运行时一旦应用有文件上传或命令执行漏洞攻击者直接拿到系统最高权限风险完全不成比例。建一个专用用户useradd -r -s /sbin/nologin www-r表示系统用户-s /sbin/nologin表示不给他登录 shell。然后把项目目录属主改成这个用户。8000 以上的端口普通用户就能监听不存在非 root 不能开端口的问题。4. Gunicorn 加 Nginx 让项目真正对外服务4.1 WSGI 服务器的选型与启动参数Python 自带的开发服务器性能差、不稳定官方文档明确说了不能用于生产。生产环境需要一个 WSGI 服务器把 Python 应用和 HTTP 请求对接起来主流是 Gunicorn 和 uWSGI 两家。Gunicorn 的优势是配置简单、文档清晰、跟 Flask/Django 集成顺滑uWSGI 的性能上限略高、功能更多但配置项多到让人头大。绝大多数项目和团队选 Gunicorn 就够了不用纠结。装进虚拟环境source venv/bin/activate pip install gunicorn启动方式gunicorn -w 4 -b 127.0.0.1:8000 wsgi:app这里的参数每一个都影响实际表现-b 127.0.0.1:8000只监听本地回环地址意味着外部不能直接访问 8000 端口所有请求必须经过 Nginx 转发。这是一种安全设计避免绕过 Nginx 直接打应用。-w 4是 worker 进程数。行业里流传的经验公式是CPU 核数 * 2 1因为 Python 有 GIL单进程同一时刻只有一个线程在跑 Python 字节码多进程才能吃满多核。但如果是 I/O 密集型大部分 Web 应用都算worker 数是够用就行开太多反而增加内存压力和上下文切换开销。1 核机器开 2 到 3 个4 核开 8 个左右然后压测看实际 QPS 调整。除了进程数还有几个参数建议加上gunicorn -w 4 -b 127.0.0.1:8000 \ --timeout 120 \ --access-logfile /data/www/myproject/logs/access.log \ --error-logfile /data/www/myproject/logs/error.log \ --daemon \ wsgi:app--timeout 120表示单个请求超过 120 秒就杀掉 worker 重启默认是 30 秒。如果你的接口里有耗时比较长的操作比如导出大报表、调用外部接口30 秒会被误杀这个值要根据业务调。如果你的应用是异步框架FastAPI 用 uvicornTornado那就不该用 Gunicorn 的 sync worker得用对应的 worker 类型或者直接用 uvicorn 配 systemd。选错 worker 类型的后果是异步优势全没了性能还不如同步框架。4.2 Nginx 反向代理配置该注意什么Nginx 在这套架构里干三件事接收 80/443 端口的外部请求、把动态请求转给 Gunicorn、直接返回静态文件。静态文件交给 Nginx 是因为它处理静态资源的效率比 Python 应用高一个数量级不需要绕一圈 Python 解释器。安装并配置yum install -y nginx systemctl enable nginx站点配置写在/etc/nginx/conf.d/myproject.confserver { listen 80; server_name your-domain.com; access_log /var/log/nginx/myproject_access.log; error_log /var/log/nginx/myproject_error.log; client_max_body_size 20m; location /static/ { alias /data/www/myproject/static/; expires 7d; } 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; proxy_read_timeout 120s; } }几个地方容易出问题一个个说。client_max_body_size默认只有 1M超过这个大小的上传请求会被 Nginx 直接挡掉并返回 413压根到不了应用。文件上传功能一上线就报错的八成是这个。设成 20m 或按业务需要调整。proxy_set_header X-Real-IP和X-Forwarded-For是给应用用的。不加这两行应用里拿到的客户端 IP 永远是127.0.0.1因为请求是 Nginx 转发的。做访问统计、风控、日志分析的项目必须加上并且在框架里正确读取。proxy_read_timeout要跟 Gunicorn 的--timeout保持协调。如果一个 120 秒另一个 30 秒会出现应用还在处理Nginx 已经返回 504的情况。两个值要么相等要么 Nginx 稍微大一点。location /static/用alias而不是root这两个指令的路径拼接规则不同。用root时实际路径会变成root值加上 location 前缀很容易多一层目录导致 404。用alias才是这个 URL 前缀直接映射到这个目录。配完先测语法再重载别直接 restartnginx -t systemctl reload nginxnginx -t能在不中断服务的情况下检查配置语法错误这个习惯能省掉不少线上事故。4.3 防火墙与 SELinux 的隐形拦截服务起在本地curl 127.0.0.1:8000 有响应但外网访问不了这类问题十有八九是防火墙或者 SELinux 在拦。CentOS 7 默认用 firewalld。放行 80 和 443firewall-cmd --permanent --add-servicehttp firewall-cmd --permanent --add-servicehttps firewall-cmd --reload firewall-cmd --list-all注意--permanent和--reload的配合。只加--permanent不 reload规则不会生效只加不加--permanent重启后规则就没了。两个一起加才是正确姿势。至于 8000 端口前面说了它只监听 127.0.0.1不需要放行也不应该放行。如果确实要临时直连调试记得调完就关掉。SELinux 是个更隐蔽的坑。它是 CentOS 默认开启的强制访问控制机制会限制 Nginx 能不能访问/data/www/下的文件也会限制进程能不能对外发起网络连接。表现就是权限都对了、防火墙也开了但就是 403。先看状态getenforce如果输出Enforcing就有可能是它在拦。查日志tail -f /var/log/audit/audit.log | grep denied如果确认是 SELinux 拦的有两条路。一是调整策略让 Nginx 能访问自定义目录setsebool -P httpd_can_network_connect 1 semanage fcontext -a -t httpd_sys_content_t /data/www/myproject/static(/.*)? restorecon -Rv /data/www/myproject/statichttpd_can_network_connect这个布尔值控制的是 Nginx 能否发起对外连接也就是能不能 proxy_pass 到本地 8000 端口。不开它Nginx 会报Permission denied while connecting to upstream日志里看着像网络问题实际是 SELinux。二是直接关掉 SELinux。我不推荐但确实有人在测试环境这么干改/etc/selinux/config里的SELINUXdisabled然后重启。生产环境关掉它等于放弃了一层重要防护能调策略就调策略。5. 用 systemd 托管进程与日志5.1 为什么 nohup 和 screen 都不该用于生产很多人部署完的启动方式是这样nohup python app.py app.log 21 这条命令能用但问题不少。服务器重启后进程不会自动拉起进程意外崩溃后没人管服务就这么挂着日志和进程的对应关系混乱查看状态只能靠ps和grep多实例时更容易搞混。screen 和 tmux 能保持会话解决了关掉终端进程就死的问题但它们本质上是给人用的交互工具不是进程管理方案。服务器重启照样丢崩溃照样不拉起。systemd 是 CentOS 7 的官方进程管理方案它解决的正是这些问题开机自启、崩溃自动重启、统一日志收集、依赖关系管理、状态查询。既然系统自带没有理由不用。5.2 Unit 文件里每个字段到底在管什么新建/etc/systemd/system/myproject.service[Unit] DescriptionMyProject Gunicorn Service Afternetwork.target [Service] Typesimple Userwww Groupwww WorkingDirectory/data/www/myproject EnvironmentPATH/data/www/myproject/venv/bin EnvironmentFile-/data/www/myproject/.env ExecStart/data/www/myproject/venv/bin/gunicorn \ -w 4 -b 127.0.0.1:8000 \ --access-logfile /data/www/myproject/logs/access.log \ --error-logfile /data/www/myproject/logs/error.log \ wsgi:app Restartalways RestartSec5 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target逐项拆解因为这些字段写错了服务就是起不来。Afternetwork.target保证网络就绪后再启动服务否则可能因为网络还没起来导致绑定失败。User和Group指定运行身份这里用前面建的 www 用户。这里有个必须注意的细节WorkingDirectory、日志目录、venv 目录这些路径 www 用户都得有读写权限。systemd 不会帮你处理权限权限不对就是Permission denied而且报错信息比较绕。EnvironmentPATH...把虚拟环境的 bin 目录加到 PATH 最前面这样 ExecStart 里即使写简写命令也能找到正确的解释器。加上这行能避掉一类找不到模块的怪问题。EnvironmentFile-/path/.env用来加载环境变量文件比如数据库密码、密钥这些不该硬编码在代码里的配置。前面那个-号表示文件不存在也不报错这个细节在 CI/CD 环境里很实用因为不同环境可能没有这个文件。ExecStart里用虚拟环境里的 gunicorn 绝对路径不用source activate那种写法。systemd 不执行 shell 的激活脚本写source一定失败。Restartalways表示无论什么原因退出都重启配合RestartSec5表示等 5 秒再拉避免疯狂重启打满 CPU。如果应用启动就崩always会导致不停重启systemctl status里能看到重启次数这个数字是你判断问题严重性的第一手信息。配好之后systemctl daemon-reload systemctl enable myproject systemctl start myproject systemctl status myprojectdaemon-reload不能省。改了 unit 文件不 reloadsystemd 用的还是旧配置改了等于没改很多人在这里反复怀疑自己是不是改错了文件。5.3 日志查看与启动失败的排查路径systemd 把服务的标准输出和错误都收进了 journal查看方式journalctl -u myproject -f journalctl -u myproject --since 10 minutes ago journalctl -u myproject -n 100 --no-pager-f是持续跟踪等价于 tail -f调试时最常用。服务起不来的排查顺序我一般是这么走的第一步看状态和退出码systemctl status myproject如果显示Active: failed并且有status203/EXEC基本是 ExecStart 路径写错或者文件没有可执行权限。203这个码的含义就是找不到或无法执行方向很明确。如果是status200/CHDIR那是 WorkingDirectory 不存在或没权限。如果是status1/FAILURE那就是应用自己启动失败了得看应用日志通常是 import 错误、配置缺失、端口被占用。第二步看应用日志tail -n 50 /data/www/myproject/logs/error.logGunicorn 的启动错误都写在这里包括哪个模块 import 失败、哪个配置项缺失、Python 语法错误在哪一行。第三步检查端口占用ss -lntp | grep 8000如果 8000 已经被别的进程占了Gunicorn 会报Address already in use。这种情况要么换端口要么先杀掉占用进程。一个特别容易漏的点systemd 启动的服务环境变量跟你 SSH 登录后的环境变量完全不同。你在终端里echo $PATH看到的东西systemd 看不到。所以任何依赖环境变量的配置都必须显式写进 unit 文件或者 EnvironmentFile不能指望它自动继承。这个问题造成的 bug 特别隐蔽因为手动跑好好的一交给 systemd 就挂。6. 上线前后最容易踩的那些坑6.1 编码、时区与中文乱码中文乱码在部署阶段出现的频率极高根源无非三类文件编码、locale 设置、数据库连接编码。Python 3 默认源码是 UTF-8这点比 Python 2 好很多。但如果代码里有从文件读取的中文而文件本身存成了 GBK读出来就是乱码。统一用 UTF-8 保存所有文件是最省事的做法。locale 影响的是系统层面的字符处理。检查当前设置locale如果LANG是空的或者POSIX中文可能在日志和命令行里显示异常。设置为 UTF-8localedef -i zh_CN -f UTF-8 zh_CN.UTF-8 echo LANGzh_CN.UTF-8 /etc/locale.conf source /etc/locale.conf如果只是想让系统按 UTF-8 处理用en_US.UTF-8也完全可以跟中文显示没冲突反而少一些兼容问题。时区问题更隐蔽。服务器默认可能是 UTC你的应用记录的时间会比北京时间少 8 小时跟数据库里的时间对不上排查起来很费劲。设置时区timedatectl set-timezone Asia/Shanghai timedatectl数据库连接层面的编码MySQL 要在连接串里指定charsetutf8mb4PostgreSQL 要在建库时指定编码。utf8mb4 比 utf8 多了对四字节字符的支持比如某些表情符号如果应用要存这些内容必须用 utf8mb4否则插入时报错或者被截断。6.2 依赖版本冲突与缓存带来的怪问题依赖问题里最让人抓狂的是本地好的服务器上不行。常见原因是两边 Python 版本不一致。比如本地用 3.11 写的代码用了新语法服务器上是 3.8直接语法错误。部署前先在服务器上python3 -V跟本地核对这一步花 10 秒能省几小时。第二个原因是 pip 缓存。pip 会把下载过的 wheel 缓存在~/.cache/pip如果某个包之前装过一半失败缓存里可能有损坏的文件导致后续安装一直报错。清缓存pip cache purge或者干脆加--no-cache-dir强制不用缓存pip install --no-cache-dir -r requirements.txt第三个原因是虚拟环境没激活就装依赖。看起来像废话但确实经常发生SSH 新开一个窗口忘了source venv/bin/activatepip install装到了全局环境然后启动服务时用的是虚拟环境的解释器自然找不到包。判断方法很简单which pip看一眼路径对不对养成这个习惯。6.3 一份可以直接照着走的上线前检查清单部署完别急着宣布完成下面这些项挨个过一遍能拦住大部分线上问题。检查项命令或方法期望结果Python 版本一致python3 -V与开发环境一致关键模块可用python3 -c import ssl, sqlite3无报错虚拟环境正确which python指向 venv 目录依赖完整pip check无依赖冲突服务状态systemctl status myprojectactive (running)本地接口curl 127.0.0.1:8000/health返回 200外部访问浏览器或 curl 域名返回正常静态资源请求一个图片或 CSS正常返回防火墙规则firewall-cmd --list-all已放行 http/https开机自启systemctl is-enabled myprojectenabled日志正常tail -f logs/error.log无异常堆栈时区正确date与本地时间一致pip check这个命令值得单独提一下它会检查已安装包之间的依赖是否满足能发现某个库要求的版本被另一个库覆盖了这类问题比运行时才报 ImportError 要早得多。还有一个经常被忽略的检查重启一次服务器看服务能不能自动起来。很多人只测了systemctl restart没测真正的重启。真出故障时机器重启后发现服务没起来那才是真的被动。选个业务低峰期做一次完整的重启验证心里才踏实。关于 Python 3.11 及更高版本还有个小细节可以用上-X frozen_modulesoff和各种启动优化参数但这些属于锦上添花不是部署的必选项先把主链路跑通再说。最后分享一个我在长期维护中形成的习惯把整个部署过程写成一个 shell 脚本提交到代码仓库里。这份脚本本身就是最准确的部署文档新人接手时不用问东问西看脚本就知道这台机器上到底装了什么、放在哪、怎么起。环境变量、目录结构、systemd 配置的模板也一并放进去。踩过的每个坑最后都会变成脚本里的一行注释这比任何口头交接都可靠。