看到"CentOS 7 部署 Django"这几个字,我第一时间想到的不是命令序列,而是那些年在生产服务器上踩过的坑:Python 2.7 和 3.x 并存、虚拟环境路径写错、Nginx 和 uWSGI 的 socket 对不上、SELinux 默默拦截反代请求……这套组合本身并不复杂,但它涉及系统库、应用服务器、Web 服务器三个层次,任何一个环节的配置有偏差,最终页面都只会回给你一个 502。这篇文章我不打算按官方文档复述一遍,而是以实际部署流程为主线,把每一步为什么要这么做的逻辑、关键参数的含义、以及我亲身遇过的坑原原本本写清楚,给正准备把 Django 项目搬上生产环境的同学一份可以直接照做的参考。
1. 整体方案拆解:这套组合里每个角色都在干什么
Django 项目真正上线之前,必须想明白一件事:开发环境下你用runserver跑得很欢,但生产环境不能这么干。runserver是开发服务器,单进程、性能弱、没有经过安全加固,直接暴露到公网等于裸奔。所以才需要把" Web 服务"拆成多个专职组件,各干各的活。
1.1 四层架构的职责划分
这套经典组合可以理解成一个餐饮店的运作流程:
- Nginx:大堂接待。负责接收所有外部 HTTP 请求,处理静态资源(图片、CSS、JS 这些"预制菜"),然后把动态请求转交给后厨。它擅长高并发、静态文件处理、超时控制,但不懂 Python。
- uWSGI:后厨主管。通过 WSGI 协议和 Django 应用直接对话,负责把 Nginx 转过来的请求变成 Django 能理解的格式,再拿着 Django 返回的响应回去。它懂 Python,懂 WSGI,是连接 Web 服务器和应用代码的桥梁。
- Django:厨师团队。真正执行业务逻辑的地方,处理完返回 HTML 或 JSON。
- virtualenv:隔离厨房。把 Django 及其依赖装在一个独立目录里,不会污染系统的 Python 环境,也不会因为系统升级导致项目跑不起来。
- Python3:烹饪工具本身。CentOS 7 系统自带的 Python 是 2.7,而现代 Django 版本早已不支持 Python 2,所以必须手动装一个 Python3。
1.2 为什么不用 Gunicorn 或者别的组合
有人会问,为什么选 uWSGI 而不是 Gunicorn?这个纯看个人偏好和团队熟悉度。uWSGI 配置项多、功能全、性能也够硬,但学习曲线确实比 Gunicorn 陡峭;Gunicorn 配置更简洁,对大多数中小项目也完全够用。我在这篇里写 uWSGI,是因为它在 Nginx 生态里的配合最传统、资料最多,而且 uWSGI 的 ini 配置一旦理解透了,换到任何项目都能快速上手。
至于 Django 自带的 runserver,上面说了,开发专用,绝对不能在生产环境用。
1.3 一个请求的完整旅程
先建立整体画面感,后面配置才不会乱。用户浏览器输入http://yourdomain.com/login/,这个请求先到达 Nginx 的 80 端口。Nginx 看了一眼路径,发现开头不是/static/,判定为动态请求,于是通过 uWSGI 协议把整个请求转发给本机的 uWSGI(通常走 socket 文件或端口)。uWSGI 载入 Django 的 WSGI 模块,Django 根据 URL 路由找到对应的视图函数,执行业务逻辑(查数据库、渲染模板等),返回一个 HTTP 响应,原路返回给 Nginx,Nginx 再交给浏览器。
如果请求的是/static/css/app.css这种静态文件,Nginx 根本不会打扰 uWSGI,直接从磁盘上把文件读出来返回,这就是"动静分离"。明白了这条链路,后面排错时就能立刻定位问题出在哪一段。
2. 环境准备:CentOS 7 上的 Python3 没有那么理所当然
2.1 先看系统自带环境
CentOS 7 是最"念旧"的系统之一,默认软件源里的 Python 永远停留在 2.7.5。Modern Django(4.x / 5.x)必须要 Python 3.8+,所以第一步就是装一个干净的 Python3。
不要动系统自带的 Python 2.7,很多系统工具(比如yum本身)都依赖它。正确做法是把 Python3 安装到一个独立目录,通过软链接暴露python3/pip3命令。
2.2 安装编译依赖
我建议直接编译安装 Python 3.9 或 3.11 源码,这样版本完全可控,也能带上优化参数。编译之前先把依赖装齐,缺任何一项都会让你在后续使用中踩到莫名其妙的坑:
yum install -y gcc gcc-c++ make wget libffi-devel zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel这里划重点:libffi-devel必须有。Python 3.8 之后的 ctypes 模块依赖 libffi,缺了它在 import ctypes 或者某些包时直接报ModuleNotFoundError: No module named '_ctypes',这个坑特别隐蔽,因为编译过程根本不会报错。
2.3 编译安装 Python3
cd /usr/local/src wget https://www.python.org/ftp/python/3.11.9/Python-3.11.9.tgz tar xf Python-3.11.9.tgz cd Python-3.11.9 ./configure --prefix=/usr/local/python3 --enable-optimizations make -j$(nproc) make install--enable-optimizations会额外做 Profile-Guided Optimization(PGO),编译时间会长一些,但运行性能更好。实测下来,1 核 2G 的小机器编译大概 15~25 分钟,可以接受。
装完后创建软链接:
ln -s /usr/local/python3/bin/python3 /usr/bin/python3 ln -s /usr/local/python3/bin/pip3 /usr/bin/pip3验证:
python3 -V pip3 -V还有个细节:此时pip3的版本可能偏老,顺手升级一下:
pip3 install --upgrade pip2.4 确认 pip 源
国内服务器强烈建议先配置 pip 国内镜像源,不然下载 Django、uWSGI 的速度会让人崩溃。创建~/.pip/pip.conf:
[global] index-url = https://mirrors.aliyun.com/pypi/simple/ trusted-host = mirrors.aliyun.com镜像地址按自己所在网络环境选,能加速就行。
3. virtualenv 隔离环境:给 Django 项目一个独立的房间
3.1 创建虚拟环境
直接用 Python3 自带的venv模块即可,不需要额外装 virtualenv 包:
mkdir -p /opt/www cd /opt/www python3 -m venv myenv创建完成后,/opt/www/myenv下会出现bin/、lib/等目录。bin/里就是这套环境专用的 Python、pip 和所有后续安装的命令行工具。进入环境:
source /opt/www/myenv/bin/activate激活后终端前面会出现(myenv)前缀,此时pip安装的包都会落到myenv/lib/python3.11/site-packages里面,和系统环境完全隔离。
这里说一句经验:虚拟环境目录建议放在/opt/www或/srv这类规范目录下,不要放在/root里。原因后面讲 Nginx 权限时会提到——Nginx 的 worker 进程以nginx用户运行,如果你的项目文件在/root下,权限会非常难搞。
3.2 安装 Django 与项目代码准备
激活虚拟环境后:
pip install django装完后通过pip show django或python -c "import django; print(django.get_version())"确认。
有两种方式把项目代码放进来:第一种是直接在虚拟环境目录下用django-admin startproject新建;第二种是从代码仓库克隆已有项目。我建议无论哪种方式,代码本身放在独立项目目录(比如/opt/www/myproject/),虚拟环境是虚拟环境,项目代码是项目代码,两者可以是平级目录。
以新建为例:
cd /opt/www django-admin startproject myproject cd myproject python manage.py migrate python manage.py createsuperuser3.3 配置 settings.py 的关键项
先把myproject/settings.py打开做三处调整,不然后面必然出问题:
ALLOWED_HOSTS:生产环境必须显式指定可访问的域名或 IP,不能留空:
ALLOWED_HOSTS = ['yourdomain.com', '你的服务器IP']STATIC_ROOT:这是 collectstatic 收集所有静态文件的目标目录,也是 Nginx 要指向的目录:
STATIC_URL = '/static/' STATIC_ROOT = '/opt/www/myproject/static'DEBUG:必须设为 False。Debug=True 时 Django 会自己处理静态文件并显示详细错误页,既影响性能,又可能泄露配置信息。但注意,DEBUG=False 之后,Django 就不再帮你托管静态文件了,必须靠 Nginx 来服务——这正是我们后面要配置的事。
改完 settings 后先跑一次 collectstatic:
python manage.py collectstatic这条命令会把所有 app 的静态文件都收集到 STATIC_ROOT 目录,后面 Nginx 直接从这个目录读文件。
导出依赖清单,方便后续在其他环境复现:
pip freeze > requirements.txt4. uWSGI 安装与 ini 配置:动态请求的接力棒
4.1 在虚拟环境里安装 uWSGI
很重要的一点:uWSGI 也要装进虚拟环境,而不是系统环境。这样uwsgi命令加载的 Python 就是虚拟环境里的 Python,才不会出现"uWSGI 找不到 Django 模块"的问题。
pip install uwsgi装好后先做一次快速验证,直接用 HTTP 模式启动 Django 项目试试水:
uwsgi --http :8000 --module myproject.wsgi --home /opt/www/myenv这里--home参数指定虚拟环境路径,uWSGI 内部的 Python 解释器就会从那里找包。如果浏览器访问http://服务器IP:8000/能出现 Django 页面,说明 uWSGI 和 Django 的配合没问题,可以进行下一步配置。
先 Ctrl+C 停掉,我们要改用 socket 模式配合 Nginx。
4.2 编写 uWSGI 的 ini 配置文件
命令行参数写一堆不好维护,生产环境建议用配置文件。在项目目录下创建uwsgi.ini:
[uwsgi] # 项目实际路径,uWSGI 会切换到该目录 chdir = /opt/www/myproject # 加载 Django 的 WSGI 应用,即 myproject/wsgi.py 里定义的 application module = myproject.wsgi:application # 虚拟环境路径 home = /opt/www/myenv # 进程相关 master = true processes = 2 threads = 2 # socket 通信,Nginx 通过它转发请求 socket = /run/uwsgi/myproject.sock # 也可以改用端口模式:socket = 127.0.0.1:8001 chmod-socket = 660 # 超时与优雅退出 harakiri = 60 vacuum = true # 日志 logto = /var/log/uwsgi/myproject.log逐项解释一下关键参数:
module = myproject.wsgi:application:这是 uWSGI 加载 Django 入口的方式;myproject.wsgi对应项目目录下 wsgi.py 文件,application是该文件里的变量名。不要把它和--http混淆,module走的是 WSGI 协议,--http是直接把响应返回给浏览器,前者才能接 Nginx。processes和threads:一个 master 进程管理多个 worker 进程,每个 worker 有多个线程。这个配置直接影响占用内存。1 核 2G 的机器建议processes = 2, threads = 2,够用且不会把内存吃爆。socket = /run/uwsgi/myproject.sock:unix socket 文件,性能比 TCP 端口好,也不暴露端口。这里chmod-socket = 660很关键,因为 Nginx 的 worker 进程是 nginx 用户,如果 socket 文件权限不够,Nginx 根本没权限写入,直接 502。vacuum = true:uWSGI 退出时自动删除 socket 文件和 pid 文件,避免残留。harakiri = 60:单个请求最多处理 60 秒,超时直接杀掉 worker,防止死循环请求拖垮整个服务。
先创建 socket 文件所需目录:
mkdir -p /run/uwsgi chown -R root:root /run/uwsgi4.3 使用 systemd 托管 uWSGI 进程
生产环境不可能手动敲命令启动 uWSGI,要用 systemd 让它开机自启、崩溃自动重启。创建/etc/systemd/system/uwsgi.service:
[Unit] Description=uWSGI instance for myproject After=network.target [Service] User=root Group=root WorkingDirectory=/opt/www/myproject Environment=PATH=/opt/www/myenv/bin ExecStart=/opt/www/myenv/bin/uwsgi --ini /opt/www/myproject/uwsgi.ini Restart=always RestartSec=10 [Install] WantedBy=multi-user.target注意ExecStart用的是虚拟环境里的完整路径/opt/www/myenv/bin/uwsgi,不要用短命令名,这样能确保 systemd 启动时用的是同一套 Python 环境。Environment=PATH是为了让 uWSGI 的子进程也能找到虚拟环境里的命令。
启动并设置开机自启:
systemctl daemon-reload systemctl start uwsgi systemctl enable uwsgi查看状态和日志:
systemctl status uwsgi journalctl -u uwsgi -f5. Nginx 配置:反向代理与静态文件服务
5.1 安装 Nginx 并编写站点配置
CentOS 7 默认源里的 Nginx 版本偏老,建议直接用官方源或者 EPEL 源安装。这里以系统源方式演示:
yum install -y nginx安装完成后,编写站点配置/etc/nginx/conf.d/myproject.conf:
server { listen 80; server_name yourdomain.com; # 客户端请求体大小,如果项目需要上传文件,按需调整 client_max_body_size 20m; # 日志 access_log /var/log/nginx/myproject_access.log; error_log /var/log/nginx/myproject_error.log; # 动态请求交给 uWSGI location / { include uwsgi_params; uwsgi_pass unix:/run/uwsgi/myproject.sock; uwsgi_read_timeout 60; } # 静态文件由 Nginx 直接处理 location /static/ { alias /opt/www/myproject/static/; } }这里有两处非常容易出错:
一是uwsgi_pass的写法。如果是 unix socket,必须写成unix:/run/uwsgi/myproject.sock,unix:前缀不能丢。如果 uWSGI 配置的是 TCP 端口,就写成uwsgi_pass 127.0.0.1:8001;。两者不能混。
二是alias的结尾斜杠。location /static/配合alias /opt/www/myproject/static/;,末尾斜杠必须对齐,否则/static/css/a.css会映射到错误路径。
配置里没有显式写uwsgi_params;吗?当成系统文件/etc/nginx/uwsgi_params里已经有了,include 进来即可。它是 Nginx 默认安装自带的,里面定义了uwsgi_param的各种参数,告诉 uWSGI 当前请求的 URL、Host、UA 等信息。
5.2 修改 Nginx 运行用户与目录权限
配置文件写好后,通常会遇到 403 或 404 问题。最常见的原因是 Nginx 的 worker 进程以nginx用户运行,而/opt/www/myproject目录对 nginx 用户没有可读权限。
我习惯把目录权限统一处理:
chown -R root:root /opt/www/myproject chmod -R 755 /opt/www/myproject # 确保 static 目录可读 chmod -R 755 /opt/www/myproject/static如果项目里还有 media 上传目录,还需要给它写权限:
mkdir -p /opt/www/myproject/media chmod -R 755 /opt/www/myproject/media/root目录默认权限是 700,如果项目放在/root下,nginx 用户根本无法进入路径的任何一级目录。这就是为什么前面建议把项目放在/opt/www这样的规范位置。如果你项目代码确实在/root下,至少要把父目录权限放宽到 711(chmod 711 /root),但我不建议这么干,安全性和规范性都差。
5.3 防火墙与 SELinux
CentOS 7 默认开启防火墙,先放行 80 端口:
firewall-cmd --permanent --add-port=80/tcp firewall-cmd --reload然后是 CentOS 7 的最大隐形坑:SELinux。默认 Enforcing 模式下,Nginx 的进程不被允许主动发起网络连接去连 uWSGI 的 socket。表现就是浏览器 502,但 uWSGI 日志正常、Nginx 日志却是权能错误。
临时关闭验证:
setenforce 0如果确认是 SELinux 的问题,正确做法不是永久关闭它(虽然网上很多教程真就这么干的),而是放行 Nginx 的对外连接:
setsebool -P httpd_can_network_connect 1如果是静态文件目录在非标准位置,还可能要用semanage fcontext设置上下文,这里先不展开,绝大多数项目放行网络连接就够了。
6. 常见问题与排查实录
部署完一套环境,不踩几个坑是不可能的。把我在部署中遇到的高频问题整理成一张速查表,每个问题都给排查思路和解决手段。
6.1 页面 502 Bad Gateway
502 是"网关错误",意思是 Nginx 连接不上 uWSGI 或者 uWSGI 没正确响应。排查路线:
- 看 uWSGI 进程是否活着:
systemctl status uwsgi - 看 socket 文件是否存在且权限正确:
ls -l /run/uwsgi/myproject.sock - 看 uWSGI 日志:
tail -f /var/log/uwsgi/myproject.log - 看 Nginx 错误日志:
tail -f /var/log/nginx/error.log
实测中最常见的两个原因:一是 socket 文件权限是 660 但属主/属组不对,nginx 用户没写权限;二是 SELinux 拦截了 Nginx 的外连,按上文放行即可。
6.2 页面 403 Forbidden
403 是"没有权限访问",通常不是 uWSGI 的问题,而是 Nginx 用户无法读取项目文件或静态文件目录。先用sudo -u nginx ls /opt/www/myproject/static/测试 nginx 用户能否列目录,不能就是目录的权限问题。逐级检查 /opt、/opt/www、/opt/www/myproject 的权限是否至少 755。
6.3 静态文件全部 404
先确认是否执行过python manage.py collectstatic,然后确认 Nginx 的alias路径和 STATIC_ROOT 是否完全一致。很多人改了配置忘记nginx -t && systemctl reload nginx,或者路径少了末尾斜杠,都会得到 404。还可以直接在服务器上 curl 一下静态文件路径,看返回的是什么状态码。
6.4 uWSGI 启动报错:ModuleNotFoundError: No module named 'django'
这个错误几乎可以肯定是 uWSGI 没有跑在虚拟环境里。检查 uwsgi.ini 里的home是否指向虚拟环境,或者直接用/opt/www/myenv/bin/uwsgi启动,而不是系统里的 uwsgi。安装 uWSGI 时注意是在激活虚拟环境的状态下 pip install 的。
6.5 页面能开,但极慢或者偶发 502
多半是超时设置不匹配。Nginx 的uwsgi_read_timeout、uWSGI 的harakiri和数据查询本身耗时共同作用。如果某个接口本身就慢(比如导出报表),把harakiri调到 120-300,uwsgi_read_timeout同步调高。同时确认 MySQL 侧有没有慢查询,别盲目调参掩盖问题。
6.6 内存不足导致 uWSGI 频繁被杀
云服务器的内存有限。2 核 2G 的机器,processes = 2, threads = 2已经让内存消耗比较可观了,如果再叠加数据库、Nginx 常驻进程,很容易 OOM。这时候一是检查服务器可用内存:free -h;二是把 uWSGI 进程数降下来:processes = 1, threads = 4,单进程多线程也是经典配法。
7. 部署完成后的验证与收尾
全部配置好后,按顺序执行一遍验证流程:
nginx -t systemctl restart nginx systemctl status nginx uwsgi curl -I http://localhost/能看到 200 状态码就说明整套链路通了,接下来再从外网访问一次域名,确认静态资源和动态页面都正常。
还有个小提醒:把 manage.py 所在目录的 pycache 清理干净,并确认项目里的.env或settings_local.py这类敏感文件不被 Nginx 直接访问。Nginx 只给/static/配了 alias,其他路径全部转发给 uWSGI 处理,所以敏感文件只要不在 static 目录下,不会被直接下载。但保险起见,建议把 secret key、数据库密码都放到环境变量里,不要写进 settings.py 提交到仓库。
在我实际部署过的项目里,还有一个小优化:给 uWSGI 的日志配置按天切割。v4 之后 uWSGI 支持log-rotate = true,配合系统 logrotate 来管理 /var/log/uwsgi 下的日志,避免日志文件无限增长占满磁盘。
另外提一嘴进程重启问题:Django 代码更新后,uWSGI 有--py-auto-reload参数可以监听文件变化自动重载,但生产环境我更推荐干脆systemctl restart uwsgi,干净利落,不会因为你改的某个文件触发了不期望的重载。Nginx 则不用频繁重启,改完配置systemctl reload nginx即可,平滑生效不掉连接。
这套部署流程我前前后后给不同项目做了不下十次,每次重新走一遍还是会发现新的细节问题。最重要的经验是:不要在云服务器上冲动关闭 SELinux 来图省事,也不要为了"性能好"把 uWSGI 进程调到机器扛不住的程度。稳扎稳打调好每一层配置,Django 生产部署其实没有多神秘。如果部署过程中遇到什么我没写到的怪问题,欢迎在评论区把日志贴出来,我在看到后会尽量帮忙分析。