CentOS部署Python项目:Gunicorn+Nginx+systemd
发布时间:2026/9/29 5:54:43 作者:尧图编辑部 阅读量:1,286

1. 部署前先想清楚这套方案到底解决什么问题1.1 从一台干净服务器到项目真正能跑中间隔着什么很多人对部署的理解停留在把代码传上去、敲个python main.py就完事。真到线上你就知道这条路走不通终端一关进程就没了换个SSH窗口项目就停摆用户访问时候端口要么访问不到要么被防火墙拦死第二天日志里一堆报错你连从哪查起都不知道。所以Linux(Centos)部署Python项目这件事核心不是把代码跑起来而是把它变成一个开机自启、崩溃能拉起、外部能访问、出问题能追溯的稳定服务。理解了这一层后面所有的工具选择才说得通。我见过太多新手把开发机上那句python app.py原封不动搬到CentOS上结果本地能访问、服务器上怎么都连不上折腾半天其实是防火墙和监听地址的问题。也见过代码传上去了、依赖装完了、进程也起来了结果服务器重启一次项目就再也没起来过因为他们压根没做进程守护。这些问题的本质都不是Python本身而是你还没把开发思维切换成运维思维。这篇内容适合的人群其实挺广刚接触Linux服务器、第一次要把自己的Python小项目Flask、Django、FastAPI都算放到CentOS上的新手做过几次部署但每次都要重新查资料、没有形成固定套路的中级开发者还有手里有台云服务器、想把自己写的爬虫、后台服务、接口服务长期挂起来跑的人。我会以CentOS 7.9为例因为它是目前装机量最大、资料最全、也最容易踩到老版本坑的一个版本把坑趟明白之后你换到CentOS 8、Rocky Linux或者别的发行版思路完全通用。1.2 技术选型不是越新越好而是越稳越省心一套能长期跑的部署方案通常由四块拼起来Python运行环境、WSGI/ASGI应用服务器、反向代理、进程守护。这四块缺一块项目都不算真正上线。为什么是这四块而不是别的我给你掰开说。Python运行环境这块CentOS 7.9自带的是Python 2.7这个版本早就停止维护了你的项目大概率是Python 3.x写的所以第一件事是把3.x装上。装法有好几种各有利弊。直接用系统包管理器装最省事但版本偏旧编译源码最灵活但耗时且容易缺依赖用conda或pyenv则是为了多版本共存和长周期维护方便。我在生产上更倾向系统里装一个稳定的3.x做基础、每个项目用独立的虚拟环境隔离依赖这样既不会有版本冲突又不用担心项目之间互相污染。应用服务器为什么不能直接用Flask自带的开发服务器因为它的定位就是调试用的单进程、性能差、没有并发处理能力官方文档明确说了不要用于生产环境。所以我们需要Gunicorn同步/异步都有或者uWSGI来扛请求。Gunicorn配置简单、文档友好是Flask/Django项目的首选uWSGI性能调优空间更大但配置复杂新手容易劝退。我一般默认上Gunicorn除非项目有特殊性能诉求。反向代理为什么需要Nginx它帮你干三件脏活累活一是处理静态文件效率比Python进程高得多二是做HTTPS终止和域名转发三是当缓冲层把慢请求、大请求挡在Python进程前面。没有Nginx你的Gunicorn直接暴露在公网上安全和性能都是裸奔状态。进程守护为什么用systemd因为它是CentOS原生自带的不用额外装supervisor配置一次开机自启、异常重启全搞定日志还能直接接到journalctl里统一查看。一个配置文件下去比supervisor那套Python生态的守护工具更贴近系统本身。把这四块想清楚你就理解了整个部署的骨架。接下来的每一步操作都是往这个骨架里填肉。2. 系统环境准备与基础加固2.1 系统版本确认和基础依赖一次装齐上服务器第一件事永远是先确认自己到底在什么系统上操作。cat /etc/redhat-release这条命令能直接告诉你系统是不是CentOS 7.9。为什么要先确认因为CentOS 7和8在包管理器上有差异7用的是yum8之后逐渐转向dnf命令能混用但行为有区别而且7.9的软件源里很多包版本偏老这直接影响你后面装Python和编译依赖的策略。确认好版本再决定用什么姿势装环境能少一半折腾。确认完版本先把基础工具和编译依赖一次性装齐。我吃过这个亏单独装Python结果编译到一半报错缺zlib、缺openssl、缺readline一个一个补太浪费时间。所以直接把大礼包怼上yum install -y gcc make zlib-devel bzip2-devel openssl-devel \ readline-devel sqlite-devel libffi-devel wget curl git vim这几行里每个包都不是随便写的。gcc和make是编译源码必须的zlib-devel和bzip2-devel关系到Python的压缩模块缺了以后pip装某些包会报错openssl-devel最容易被忽略缺了它Python的ssl模块就是坏的pip连HTTPS源都连不上libffi-devel是ctypes相关模块的依赖很多库安装时报ffi.h not found就是缺它。一次性装齐比一个个试错强太多。注意如果你用的是CentOS 7.9的最小化安装镜像可能连wget都没装。可以先用curl或者用yum install -y wget补上。别小看这个细节很多人第一步就卡在这。2.2 用户权限、防火墙和端口这几关必须提前过个人项目图方便可以一直用root但只要你想稍微正规一点强烈建议新建一个普通用户来跑项目别把整个服务器交给应用去折腾。为什么因为程序一旦有漏洞被利用root权限意味着对方可以直接接管整台机器普通用户最多只能在自己目录里活动损失可控。建用户很简单useradd -m deploy passwd deploy usermod -aG wheel deploy-m是给它建家目录wheel组是为了必要时能通过sudo提权。建好之后项目代码、虚拟环境、日志全部放在这个用户的家目录下权限边界清晰日后排查也方便。接下来是让无数新手翻车的防火墙。CentOS 7默认用的是firewalld你的Gunicorn监听在某个端口Nginx监听80如果防火墙没放行你在本地浏览器里永远看到的是无法访问。放行命令是这样的firewall-cmd --zonepublic --add-port80/tcp --permanent firewall-cmd --zonepublic --add-port8000/tcp --permanent firewall-cmd --reload--permanent表示永久生效不加的话重启就没了--reload让配置立即加载。这里有个实操心得8000端口别长期对外开放它只是给你自己调试用的正式上线后应该只让Nginx访问它、对外只开80和443。具体做法是把Gunicorn绑定到127.0.0.1:8000而不是0.0.0.0:8000这样外部根本连不到这个端口安全又干净。除了系统防火墙云服务器通常还有一层安全组控制台里单独配置的。很多人系统防火墙放行了却还是连不上八成是安全组没开对应端口。这两层防火墙互相独立缺一不可排查访问问题时一定要两层都看。还有SELinux这个老熟人。CentOS 7默认是enforcing模式它可能拦住Nginx访问你的项目目录、拦住Nginx反向代理到8000端口。新手遇到配置都对但就是502时优先怀疑它。临时设成宽松模式验证getenforce # 查看当前状态 setenforce 0 # 临时关闭重启失效如果关掉之后立刻能访问了那就是SELinux在作祟。生产环境不建议直接永久关闭更优雅的做法是加策略或者用chcon给目录打上正确的安全上下文标签。但如果你只是个个人小项目、不搞那么严谨永久关掉也能接受改/etc/selinux/config把它设成permissive即可。2.3 Python到底怎么装我把几种方案摊开讲系统自带的Python 2.7不能动因为yum等系统工具依赖它你一旦覆盖整个系统的包管理可能直接瘫痪——这是无数人用血泪换来的教训。正确做法是装一个Python 3让它和2.7并存通过python3命令调用。第一种方案用发行版自带的Python 3。CentOS 7.9的yum源里其实有python3直接yum install -y python3就能装上版本大概是3.6。优点是不用编译、秒装、稳定缺点是版本偏旧某些新库可能要求3.8你就会被卡住。如果你项目依赖不高这条路最省心。第二种方案源码编译。我想要一个确定的、可以在多台机器上复现的版本就会选它。下载、解压、配置、编译、安装一条龙wget https://www.python.org/ftp/python/3.11.9/Python-3.11.9.tgz tar -zxvf Python-3.11.9.tgz cd Python-3.11.9 ./configure --prefix/usr/local/python3.11 --enable-optimizations make -j$(nproc) make altinstall--prefix指定安装路径避免和系统的Python 2.7冲突--enable-optimizations会做一些编译期优化让运行时性能更好代价是编译时间变长make -j$(nproc)用满所有CPU核心加速编译最关键的是make altinstall而不是make installaltinstall不会覆盖系统的python软链接只会生成python3.11这就是保命操作。装完把路径加进环境变量或者软链一下就行。第三种方案用conda或pyenv管理多版本。适合一台服务器上跑多个不同Python版本项目的场景隔离性好、切换方便代价是多一层抽象、偶尔会有环境相关的玄学问题。我个人在单项目、单版本场景下不会引入它能简则简。三种方案我都用过个人小项目我推荐第二种源码编译装一个3.10或3.11一劳永逸追求快速上手就用第一种yum装。选哪个不重要重要的是找到你项目requirements.txt里要求的版本下限别装了个3.6结果项目要3.9装之前先去项目里翻一眼。3. 代码上传与依赖环境隔离3.1 代码怎么搬到服务器上各有各的适用场景代码上传这件事落到实操有几种常见姿势我按使用频率排一下。最常用的是Git拉取。服务器上直接git clone你托管在代码仓库的私有或公开项目以后更新只要git pull版本清晰、回滚方便。这也是我强烈推荐的方式前提是你的代码有托管在某个仓库里。私有仓库记得配置好免密用deploy key或者访问令牌不然每次拉取都要输密码自动化脚本就没法写。第二种是scp/sftp手动传。适合临时改动、小文件、或者项目还没进仓库的阶段。命令简单scp -r ./myproject deploy服务器IP:/home/deploy/-r递归复制整个目录。缺点是每次改动都得手动传项目一大就容易漏文件不适合长期维护。第三种是在服务器上直接开发用vim或者VS Code的远程连接功能直接改。适合小脚本类项目但代码没有本地备份服务器一挂可能就丢了。可以配合rsync做增量同步比scp快尤其适合频繁更新、只改了几个文件的场景rsync -avz --exclude venv --exclude __pycache__ ./myproject/ deploy服务器IP:/home/deploy/myproject/-a保留权限和时间戳-v显示过程-z压缩传输--exclude把虚拟环境和缓存目录排除掉避免把本地一堆垃圾同步上去。这个排除很重要本地虚拟环境和服务器上的路径、Python版本可能不一样传上去反而出错。不管你用哪种方式上传之后都要改一下目录所属用户让deploy用户拥有读写权限chown -R deploy:deploy /home/deploy/myproject权限不对的话后面装依赖、写日志会处处报Permission denied。3.2 虚拟环境是底线不是可选项很多人图省事直接用系统的pip装依赖结果项目A要requests 2.20项目B要requests 2.31装一个另一个就挂更糟的是污染了系统Python连系统工具都可能受影响。虚拟环境必须用这不是讲究是底线。在项目目录里一条命令搞定cd /home/deploy/myproject python3 -m venv venv source venv/bin/activate执行完你会看到命令行前面多了个(venv)说明你已经进入隔离环境了。这时候pip install的任何东西都只装在项目目录里跟系统和其他项目完全隔离。装依赖前先检查项目里有没有requirements.txt有就直接pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple后面那个-i是换国内镜像源能显著提升下载速度。我实测过同一个包用默认源可能要卡几分钟甚至超时换镜像后几秒就下来了。常见镜像源还有阿里云、腾讯云都可以哪个快用哪个。注意一定要在激活虚拟环境的状态下装依赖。我见过有人忘了source venv/bin/activate结果依赖装到了系统里然后运行时报ModuleNotFoundError查半天查不出来。养成习惯进项目目录先激活环境装完用pip list确认一下包在不在当前环境。3.3 依赖安装的坑和提速技巧依赖安装这步看着简单其实是踩坑重灾区。第一种坑是编译型依赖装不上。像psycopg2、mysqlclient、Pillow、cryptography这类包需要用C语言编译如果你的编译依赖前面装的那一堆-devel包没装齐就会报command gcc failed。解决办法就是回到第2.1节把基础依赖装全。有时候缺的是某个库特定的头文件看报错里的xxx.h: No such file or directory用yum provides */xxx.h就能查到对应的-devel包。第二种坑是pip版本太旧。虚拟环境自带的pip版本可能比较老装新格式的包会报错。养成习惯激活环境后先升级一下pip install --upgrade pip setuptools wheel第三种坑是依赖版本不锁死。requirements.txt里如果只写flask不写flask2.3.3过一段时间服务器上装出来的版本可能和你本地完全不同出现本地跑得好好的服务器就报错的诡异现象。上线前把依赖版本全部锁定可以用pip freeze requirements.txt生成一份精确版本清单。第四种坑是在服务器上直接编译重型依赖超时。有些包尤其是含C扩展的服务器CPU弱、内存小编译要几十分钟甚至编译到一半被OOM杀掉。我一般的策略是在本地把wheel包下好传上去或者用pip download在同类环境里预下载再离线安装。如果项目里有用到pandas、numpy这种大包提前规划好会更省心。装完之后用pip list核对一遍关键依赖版本跟本地开发环境做比对确保一致。这一步花两分钟能省掉后面几小时的排查。4. 让项目常驻应用服务器配置与进程守护4.1 Gunicorn的启动参数不是随便填的激活虚拟环境后把Gunicorn装上pip install gunicorn启动命令一般长这样gunicorn -w 4 -b 127.0.0.1:8000 app:app这里的每个参数都有讲究。-w 4是worker进程数官方推荐的公式是(2 * CPU核心数) 1。假设你的服务器是2核那2*215可以设成4到5个worker。为什么是这个公式因为一个worker处理请求时可能因为IO等待而阻塞多开几个worker可以让CPU在等待期间去处理别的请求。但也不是越多越好worker太多会争抢内存和CPU上下文切换反而变慢。小内存服务器1G我建议先设2跑起来看内存占用再调。-b 127.0.0.1:8000是绑定地址前面强调过绑到本地回环地址外部访问不到只有本机的Nginx能连安全。如果你现在还没配Nginx临时想验证一下可以先绑0.0.0.0:8000验证完记得改回来。app:app这个写法要重点解释第一个app是模块名也就是你的主文件名比如app.py第二个:app是Flask实例的名字代码里app Flask(__name__)那个app。如果你是Django项目写法就不一样是项目名.wsgi:application。很多人启动报Failed to find application object就是这一块写错了。实际生产里参数多了以后命令行会很长很乱所以我习惯把它们写进配置文件gunicorn.conf.pybind 127.0.0.1:8000 workers 4 worker_class sync # 同步worker适合普通Web应用 timeout 60 # 单个请求超时时间 keepalive 5 # 长连接保持时间 accesslog /home/deploy/logs/gunicorn_access.log errorlog /home/deploy/logs/gunicorn_error.log loglevel info配置文件方式的好处是清晰、可版本管理、改参数不用动启动命令。worker_class如果你用的是FastAPI这种异步框架可以换成uvicorn.workers.UvicornWorker性能会好很多。实操心得timeout这个值要根据你的业务来调。如果你的接口里有耗时的任务比如调用第三方API、跑数据默认30秒很容易触发超时把worker杀掉请求直接502。把timeout调大能缓解但更好的做法是把耗时任务丢到后台队列里异步处理别让HTTP请求傻等。4.2 用systemd把服务管起来这是最稳的一步Gunicorn手动启动有个致命问题你一关SSH它就被系统当成终端子进程回收了。就算用nohup挂后台服务器重启后也不会自动起来。所以必须交给systemd托管。在/etc/systemd/system/下建一个myproject.service文件[Unit] DescriptionMy Python Project Gunicorn Service Afternetwork.target [Service] Typesimple Userdeploy Groupdeploy WorkingDirectory/home/deploy/myproject EnvironmentPATH/home/deploy/myproject/venv/bin ExecStart/home/deploy/myproject/venv/bin/gunicorn -c /home/deploy/myproject/gunicorn.conf.py app:app Restartalways RestartSec5 [Install] WantedBymulti-user.target我逐项说一下关键点。Afternetwork.target保证它在网络就绪后再启动不然可能因为网络没起来导致绑定失败。User和Group指定用哪个用户跑这就对应前面新建的deploy用户安全。WorkingDirectory是工作目录很多项目是相对路径读配置文件不设这个会找不到文件。Environment把虚拟环境的bin目录加进PATH这样ExecStart里能直接找到gunicorn。ExecStart写绝对路径最保险。Restartalways是核心进程无论什么原因退出都自动拉起RestartSec5是重启前等5秒避免疯狂重启刷日志。然后三步走systemctl daemon-reload # 重载配置 systemctl start myproject # 启动 systemctl enable myproject # 开机自启之后你就可以用systemctl status myproject看运行状态用systemctl restart myproject重启用journalctl -u myproject -f实时看日志。这套流程比supervisor顺手太多而且是系统原生的不用额外维护。日志怎么看也很关键。systemd管理下程序输出到stdout的错误都会进journalctl你用journalctl -u myproject --since 10 minutes ago就能看最近10分钟的日志排查线上问题特别方便。4.3 Nginx反向代理配置把用户挡在正确的门口Gunicorn跑起来之后我们需要Nginx对外提供服务把用户请求转发给本地的Gunicorn。先装Nginxyum install -y nginx systemctl start nginx systemctl enable nginx然后在/etc/nginx/conf.d/下建一个myproject.confserver { listen 80; server_name your-domain.com; 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 90; } location /static/ { alias /home/deploy/myproject/static/; expires 7d; } }proxy_pass把请求转发给Gunicorn这是核心。那几个proxy_set_header不是可有可无的摆设不加X-Real-IP和X-Forwarded-For你的应用日志里记录的全是127.0.0.1根本看不到真实用户IP不加X-Forwarded-Proto应用会以为所有请求都是HTTP生成的重定向链接可能出问题。location /static/把静态文件交给Nginx直接处理不经过Python进程效率高出几个数量级。expires 7d让浏览器缓存静态资源7天减轻服务器压力。注意这里的路径要和Django的STATIC_ROOT或Flask的静态目录对应上。配置改完用nginx -t检查语法没问题再systemctl reload nginx平滑重载。nginx -t这一步千万别省配置写错了reload会导致整个Nginx挂掉如果线上有别的服务也一起遭殃。到此四块拼图就齐了用户访问80端口 → Nginx接收 → 转发给本地8000端口的Gunicorn → Gunicorn用Python运行你的项目 → systemd保证它永不掉线。整套链路清晰、各司其职。5. 上线验证与常见问题排查实录5.1 排查问题要有分层思路别东一榔头西一棒子部署完访问不了是最常见的情况新手往往一通乱改越改越乱。我的经验是按照数据流的反方向逐层排查每层都有明确的验证方法定位问题飞快。第一层看进程在不在。systemctl status myproject如果显示failed或者根本没跑起来那就是应用层问题先去看journalctl -u myproject里的报错。常见的是Python依赖没装齐、启动命令里模块名写错、配置文件路径不对。第二层看端口在不在监听。ss -lntp | grep 8000如果看不到8000端口说明Gunicorn没成功绑定。可能是端口被占用也可能是绑定地址写错。用netstat -tlnp | grep 8000也能查。这一步能确认应用是不是真的在等请求。第三层在服务器本机curl测试。curl http://127.0.0.1:8000如果本机能通说明应用没有问题问题在Nginx或者网络如果本机都不通那问题一定在应用或者端口本身。这一步是分水岭能帮你把问题范围砍掉一半。第四层测Nginx。curl -I http://127.0.0.1看返回的HTTP状态码。返回502通常是Nginx连不上Gunicorn地址、端口、SELinux问题返回404是Nginx找不到对应location返回403往往是权限或SELinux问题。第五层才是外部网络。如果服务器本机全通但你在自己电脑上访问不了那就是防火墙或安全组的问题回到第2.2节检查。这个从内到外的排查顺序非常关键能让你每一步都有明确结论而不是瞎猜。5.2 高频问题速查表下面这张表是我部署多年攒下来的遇到问题先对号入座能省大量时间。现象最可能的原因快速验证与解决外部访问超时本机curl正常防火墙或云安全组没放行80端口firewall-cmd --list-ports查规则控制台查安全组报502 Bad GatewayNginx连不上Gunicorn检查Gunicorn是否监听、地址端口是否一致、SELinux是否拦截报403 Forbidden目录权限或SELinux上下文不对ls -l看权限归属chcon -R -t httpd_sys_content_t 目录ModuleNotFoundError依赖没装在当前虚拟环境激活venv后pip list核对重新pip install -r关掉SSH项目就停没用systemd托管配置service文件并systemctl enable服务器重启后项目没起来enable没执行或service有依赖错误systemctl is-enabled myproject确认看journalctlpip安装超时/极慢默认源太慢加-i换国内镜像源编译类依赖装不上缺编译工具或头文件装gcc、make和对应-devel包看报错里的.h文件名应用日志里IP全是127.0.0.1Nginx没传真实IP头加X-Real-IP和X-Forwarded-For页面样式丢失但接口正常静态文件没配好或Nginx没权限检查location /static/路径和目录权限报错端口已被占用有别的进程占了8000ss -lntp中文文件下载乱码文件名编码问题系统locale设成UTF-8localectl set-locale LANGzh_CN.UTF-8这张表里每一个我都在真实环境里撞过。尤其提醒一句本机能通、外部不通这个模式99%是防火墙问题别浪费时间在代码上找。5.3 几个我踩过坑才总结出来的细节最后分享几个常规教程里不太会写、但实际很影响体验的点。第一个是日志轮转。Gunicorn的访问日志和错误日志会随着时间无限增长跑几个月能把磁盘撑爆。我见过项目因为日志文件涨到几十G把服务器磁盘写满然后整个应用崩掉。解决办法是配置logrotate在/etc/logrotate.d/下加一个配置让它按天切割、保留最近7天、自动压缩。这件事上线前就该做别等出事。第二个是环境变量管理。数据库密码、API密钥这种敏感信息不要硬编码在代码里也别直接写在systemd的ExecStart里会被ps看到。用systemd的EnvironmentFile指向一个只有deploy用户能读的文件权限设成600既安全又好维护。这个习惯一旦养成日后迁移或者改配置都很清爽。第三个是部署脚本化。第一次部署可能会手动敲几十条命令第二次一定要把它写成一个脚本。哪怕就是一个shell文件把拉代码、装依赖、重启服务这几步串起来以后更新版本就是执行一个脚本的事减少手抖出错。我后来干脆把它做成一个简单的CI流程推送代码自动触发部署省心得多。第四个是版本回滚方案。上线最怕改坏了没法快速恢复。我的做法是每次部署前给当前版本打个tag或者备份一份代码目录出问题能一键切回去。Gunicorn的reload是平滑的切版本时用户体验基本无感。别小看这个准备关键时刻能救命。这套流程我在好几个CentOS 7.9的机器上反复跑过从小型Flask接口到稍复杂的Django后台都适用。真正动手的时候你会发现在命令行敲下systemctl enable那一刻、看着服务状态变成active (running)、再从浏览器里访问到自己的项目那种它终于稳了的感觉比本地跑起来强太多。部署这门手艺文档看得再多不如自己完整走一遍踩几个坑这套东西就真长在你身上了。