ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Django商城生产部署全链路指南:Nginx+Gunicorn+Supervisor实战

Django商城生产部署全链路指南:Nginx+Gunicorn+Supervisor实战

简介:本资源是一套面向中高级Python Web开发者的学习型Django电商项目,聚焦Linux生产环境下的大型前后端商城完整搭建实践,适用于希望深入理解Django架构设计、部署流程与高并发电商场景开发的工程师与进阶学习者。压缩包共204个文件,涵盖99个核心Python模块(含models、views、settings等)、30个HTML模板页(商品列表、购物车、结算页等)、24张JPG/PNG商品及界面截图、18个JS交互脚本、20个CSS样式文件及配套字体、SQL数据库初始化脚本等,整体1.69MB,结构清晰,便于按层拆解学习。目前已有232人下载学习,可直接运行调试,助你掌握Linux下Django项目环境配置、MySQL数据库集成、用户认证体系、RESTful接口设计、模板渲染逻辑及Nginx+Gunicorn部署链路等关键能力,是少有的兼顾工程规范性与教学可读性的实战型商城基线代码。

1. 这不是又一个“Django博客教程”:它是一套跑在真实Linux服务器上的高可用商城骨架,专治「本地能跑、上线就崩」的血泪现场

你有没有试过:本地用python manage.py runserver跑得飞起,商品页秒开、购物车实时更新、支付回调日志刷屏;可一上 Linux 服务器,gunicorn启动报ImportError: No module named 'django',Nginx 返回 502 Bad Gateway,静态文件全 404,连 admin 登录页都加载不出 CSS?这不是玄学,是环境链路断在了你没看见的地方——Python 版本混用、虚拟环境未激活、STATIC_ROOT和collectstatic路径错配、DEBUG=False下的 ALLOWED_HOSTS 漏配、数据库连接池超时……这个项目,就是把所有这些「部署黑匣子」全拆开、标好序号、塞进一个可一键复现的 Linux 环境里。它不教你怎么写 Model,而是告诉你supervisord怎么盯住celery beat进程不死;不讲模板继承语法,而是实打实给出nginx.conf里/media/和/static/的 location 块怎么写才不和 Django 的whitenoise冲突;它面向的是已经写完商城核心逻辑、正卡在上线前最后一公里的开发者——你不需要重学 Django,只需要知道「在 CentOS 7 或 Ubuntu 22.04 上,哪几行配置改错,整个商城就变白屏」。

2. 架构全景与目录解剖:从requirements.txt到supervisord.conf的完整链路

这个项目不是单个 Django App,而是一个经过生产环境验证的分层部署包。它包含四个明确职责的物理目录,全部压缩在一个.tar.gz包中,解压即得完整结构:

django-mall-prod/ ├── app/ # Django 项目源码(含 settings_prod.py) ├── conf/ # 所有外部配置:Nginx、Gunicorn、Supervisor、Redis、Celery ├── scripts/ # 部署脚本:一键安装依赖、收集静态、重启服务 └── docs/ # 部署检查清单(checklist.md)与常见错误速查表

提示:app/目录下没有manage.py在根目录——它被移到了app/src/下,这是为避免gunicorn启动时误将app/当作 Python 包导入。这种细节,正是本地开发和线上部署的分水岭。

2.1 核心配置分离:settings_prod.py的 5 个生死开关

Django 的settings.py是部署成败的核心。本项目强制拆出settings_prod.py,并只保留生产必需项。以下是必须修改的 5 个字段,漏改任意一个都会导致服务不可用:

# app/src/mall/settings_prod.py import os from .settings import * # 1. DEBUG 必须为 False(否则 500 错误不显示详细信息,但更致命的是安全策略失效) DEBUG = False # 2. ALLOWED_HOSTS 必须精确匹配你的域名或 IP(不能留空,不能写 *) ALLOWED_HOSTS = ['mall.example.com', '192.168.10.50'] # ← 替换为你的真实域名/IP # 3. 静态文件根目录:指向 Nginx 将要服务的绝对路径 STATIC_ROOT = '/var/www/mall/static/' # ← 此路径需由 collectstatic 生成,且 Nginx 有读权限 # 4. 媒体文件根目录:用户上传的图片、PDF 等 MEDIA_ROOT = '/var/www/mall/media/' # ← 此路径需由 Nginx 显式配置 location /media/ # 5. 数据库密码不再硬编码,从环境变量读取(避免泄露到 Git) DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'mall_db', 'USER': 'mall_user', 'PASSWORD': os.environ.get('DB_PASSWORD', 'dev_password'), # ← 生产环境必须设环境变量 'HOST': '127.0.0.1', 'PORT': '5432', } }

逻辑说明:DEBUG=False不仅关闭调试信息,还会触发 Django 对ALLOWED_HOSTS的严格校验;STATIC_ROOT和MEDIA_ROOT是 Django 内部路径,而 Nginx 的location块必须指向同一物理位置,否则浏览器请求/static/css/app.css时 Nginx 找不到文件,返回 404;os.environ.get()是安全读取密码的标准做法,部署时通过export DB_PASSWORD="xxx"设置即可。

2.2 Nginx 配置:为什么/static/和/media/必须独立 location?

conf/nginx/mall.conf是本项目最易翻车的配置文件。新手常犯的错误是把所有请求都 proxy_pass 给 Gunicorn,结果静态资源走 Python 处理,性能暴跌且易超时。正确做法是让 Nginx 直接服务静态和媒体文件,只把动态请求(如/api/,/admin/,/checkout/)转发给后端:

# conf/nginx/mall.conf upstream django_app { server 127.0.0.1:8000; # gunicorn 监听地址 } server { listen 80; server_name mall.example.com; # 1. 静态文件:Nginx 直接返回,不走 Django location /static/ { alias /var/www/mall/static/; # ← 注意末尾斜杠!alias 要求路径结尾带 / expires 1y; add_header Cache-Control "public, immutable"; } # 2. 媒体文件:用户上传内容,同样由 Nginx 直接提供 location /media/ { alias /var/www/mall/media/; # ← 同样注意末尾斜杠 expires 7d; } # 3. 动态请求:全部 proxy_pass 给 Gunicorn 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; } }

参数说明:alias指令会完全替换匹配的 URI 路径,所以location /static/+alias /var/www/mall/static/表示请求/static/css/app.css时,Nginx 去读/var/www/mall/static/css/app.css;而root指令是拼接路径,容易出错。expires和Cache-Control是性能关键,避免浏览器重复请求不变的 JS/CSS。proxy_set_header确保 Django 能拿到真实客户端 IP,否则request.META.get('REMOTE_ADDR')永远是127.0.0.1。

2.3 Gunicorn 与 Supervisor:进程守护的黄金搭档

gunicorn是 WSGI 服务器,负责把 HTTP 请求转成 Django 可理解的 Python 调用;supervisord是进程管理器,确保gunicorn和celery worker即使崩溃也能自动拉起。二者缺一不可。

conf/gunicorn/mall.conf定义了 Gunicorn 的启动参数:

# conf/gunicorn/mall.conf command=/var/www/mall/venv/bin/gunicorn args=--bind 127.0.0.1:8000 \ --workers 3 \ --worker-class sync \ --timeout 120 \ --keep-alive 5 \ --max-requests 1000 \ --max-requests-jitter 100 \ --log-level info \ --log-file /var/log/mall/gunicorn.log \ --pid /var/run/mall/gunicorn.pid \ --chdir /var/www/mall/app/src/ \ mall.wsgi:application user=www-data group=www-data

逻辑说明:--workers 3是经验公式(CPU 核数 × 2 + 1),Ubuntu 22.04 默认 2 核,故设 3;--timeout 120防止长事务(如导出万条订单)被误杀;--chdir必须指向wsgi.py所在目录,否则mall.wsgi:application导入失败;--pid文件用于supervisor控制启停。user/group必须是 Nginx 运行用户(通常是www-data),否则 Nginx 无法与 Gunicorn socket 通信。

conf/supervisor/mall.conf则定义了如何守护它:

# conf/supervisor/mall.conf [program:mall-gunicorn] command=/var/www/mall/venv/bin/gunicorn --config /var/www/mall/conf/gunicorn/mall.conf mall.wsgi:application directory=/var/www/mall/app/src/ user=www-data autostart=true autorestart=true redirect_stderr=true stdout_logfile=/var/log/mall/gunicorn_supervisor.log [program:mall-celery-worker] command=/var/www/mall/venv/bin/celery -A mall worker -l info -Q default,high_priority directory=/var/www/mall/app/src/ user=www-data autostart=true autorestart=true redirect_stderr=true stdout_logfile=/var/log/mall/celery_worker.log

关键点:autorestart=true是「后悔药」,当 Gunicorn 因内存溢出崩溃时,supervisor会在 1 秒内拉起新进程,用户无感知;stdout_logfile日志路径必须提前创建并赋权(sudo mkdir -p /var/log/mall && sudo chown www-data:www-data /var/log/mall),否则supervisor启动失败。

3. 一键部署实战:从系统初始化到首页可访问的 7 个确定性步骤

部署不是「试试看」,而是按顺序执行的确定性流程。本项目附带scripts/deploy.sh,但你必须理解每一步在做什么,才能在出错时精准定位。

3.1 系统准备:Ubuntu 22.04 的最小化初始化

我们以 Ubuntu 22.04 为例(CentOS 7 步骤见docs/os-compatibility.md)。先确保系统干净:

# 1. 更新系统并安装基础工具 sudo apt update && sudo apt upgrade -y sudo apt install -y python3-pip python3-venv nginx supervisor postgresql postgresql-contrib redis-server git curl # 2. 创建部署用户(非 root,符合安全规范) sudo adduser --disabled-password --gecos "" malluser sudo usermod -aG sudo malluser sudo su - malluser # 3. 创建项目根目录并赋权 sudo mkdir -p /var/www/mall sudo chown -R malluser:www-data /var/www/mall sudo chmod -R 775 /var/www/mall

逻辑说明:malluser是部署操作者,www-data是 Nginx/Gunicorn 运行用户,chown -R malluser:www-data让两者都能读写/var/www/mall;chmod 775确保组成员(www-data)有写权限,这是collectstatic能成功写入/var/www/mall/static/的前提。

3.2 代码与依赖:虚拟环境隔离是唯一正解

切勿用系统 Python 或全局 pip!必须用venv:

# 进入项目目录 cd /var/www/mall # 1. 解压源码(假设已上传 django-mall-prod.tar.gz) tar -xzf ~/django-mall-prod.tar.gz --strip-components=1 # 2. 创建并激活虚拟环境 python3 -m venv venv source venv/bin/activate # 3. 安装 Python 依赖(注意:用 requirements_prod.txt,不是开发版) pip install --upgrade pip pip install -r app/requirements_prod.txt # 4. 验证 Django 是否可导入(关键检查!) python -c "import django; print(django.__version__)" # 应输出 4.2.7 或类似版本,若报错则 pip install 失败

参数说明:requirements_prod.txt里移除了django-debug-toolbar、django-extensions等开发依赖,只保留psycopg2-binary(PostgreSQL 驱动)、gunicorn、celery[redis]、whitenoise(用于开发环境静态服务,生产环境由 Nginx 接管)等生产必需包。pip install -r后必须手动验证import django,这是防止venv激活失败或路径错乱的最简哨兵。

3.3 数据库与静态文件:两步落地,缺一不可

# 1. 创建 PostgreSQL 数据库与用户(切换到 postgres 用户) sudo su - postgres psql # 在 psql 中执行: CREATE DATABASE mall_db; CREATE USER mall_user WITH PASSWORD 'strong_password_here'; GRANT ALL PRIVILEGES ON DATABASE mall_db TO mall_user; \q exit # 2. 设置环境变量(永久生效) echo 'export DB_PASSWORD="strong_password_here"' >> ~/.bashrc source ~/.bashrc # 3. 运行 Django 迁移(必须在 venv 激活状态下) cd /var/www/mall/app/src/ source /var/www/mall/venv/bin/activate python manage.py migrate # 4. 收集静态文件(关键!此步生成 STATIC_ROOT 目录) python manage.py collectstatic --noinput # 5. 验证静态文件是否生成 ls -la /var/www/mall/static/ # 应看到 css/, js/, images/ 等目录

逻辑说明:collectstatic是部署中最易被忽略的步骤。它把所有 App 的static/目录下的文件,按STATICFILES_DIRS和INSTALLED_APPS顺序,合并拷贝到STATIC_ROOT(即/var/www/mall/static/)。如果这步没做,Nginx 去/var/www/mall/static/找不到任何文件,所有 CSS/JS 都 404。--noinput参数避免交互式确认,适合脚本化。

4. 避坑指南:那些让你凌晨三点还在查日志的 5 个真实翻车现场

部署不是线性的,而是充满「现象 → 原因 → 解决」的循环。以下是本项目用户反馈最集中的 5 个问题,每一条都来自真实血泪经验。

4.1 现象:Nginx 启动成功,但访问域名返回 502 Bad Gateway

原因:gunicorn进程根本没起来,或者监听地址与 Nginxproxy_pass不匹配。常见于gunicorn.conf中--bind地址写成0.0.0.0:8000(暴露给外网,不安全)或127.0.0.1:8001(Nginx 配置却是8000)。
解决:

  1. 查supervisor状态:sudo supervisorctl status,看mall-gunicorn是否为RUNNING;
  2. 若为FATAL,查日志:sudo tail -50 /var/log/mall/gunicorn_supervisor.log;
  3. 最常见原因是ImportError,检查venv是否激活、PYTHONPATH是否正确、chdir路径是否存在。

4.2 现象:网页打开,但所有按钮点击无反应,控制台报Failed to load resource: the server responded with a status of 404 (Not Found),路径为/static/js/app.js

原因:Nginx 的location /static/配置错误,或STATIC_ROOT路径与alias不一致。
解决:

  1. 确认collectstatic已执行,且/var/www/mall/static/js/app.js物理存在;
  2. 检查 Nginx 配置中alias路径末尾是否有/(必须有!);
  3. 手动测试:curl http://localhost/static/js/app.js,若返回 404,则是 Nginx 配置问题;若返回文件内容,则是 Django 的STATIC_URL配置错误(应为/static/)。

4.3 现象:python manage.py migrate报错django.db.utils.OperationalError: FATAL: password authentication failed for user "mall_user"

原因:PostgreSQL 用户密码未正确设置,或pg_hba.conf未允许md5认证。
解决:

  1. 切换到postgres用户:sudo su - postgres;
  2. 进入psql,执行\password mall_user重设密码;
  3. 编辑/etc/postgresql/*/main/pg_hba.conf,确保有:
    local all all md5
    host all all 127.0.0.1/32 md5
  4. 重启 PostgreSQL:sudo systemctl restart postgresql。

4.4 现象:supervisorctl start mall-gunicorn后立即变成FATAL,日志显示OSError: [Errno 13] Permission denied: '/var/run/mall'

原因:/var/run/mall目录不存在,或www-data用户无写权限。/var/run是内存文件系统,重启后消失,不能靠mkdir一次解决。
解决:

  1. 创建持久化目录:sudo mkdir -p /var/www/mall/run;
  2. 修改gunicorn.conf中--pid路径为/var/www/mall/run/gunicorn.pid;
  3. 赋权:sudo chown -R www-data:www-data /var/www/mall/run。

4.5 现象:后台任务(如发送邮件、生成报表)不执行,Celery worker 日志为空

原因:supervisor未启动mall-celery-worker,或 Redis 服务未运行,或CELERY_BROKER_URL在settings_prod.py中指向错误地址。
解决:

  1. 检查 Redis:sudo systemctl status redis-server,确保active (running);
  2. 检查supervisor:sudo supervisorctl status,确认mall-celery-worker为RUNNING;
  3. 进入venv,手动启动 Celery 测试:celery -A mall worker -l info,看是否连接 Redis 成功;
  4. 确认settings_prod.py中CELERY_BROKER_URL = 'redis://127.0.0.1:6379/0'。

5. 二次开发改造:如何安全地接入你自己的支付网关与短信服务

部署跑通只是起点。真正的价值在于快速接入业务系统。本项目预留了清晰的扩展接口,无需动核心代码,只需覆盖配置与实现类。

5.1 支付网关接入:替换payment/模块的 3 个抽象方法

项目已抽象出payment/base.py,定义了统一接口:

# app/src/payment/base.py from abc import ABC, abstractmethod class PaymentGateway(ABC): @abstractmethod def create_order(self, order_id: str, amount: int, subject: str) -> dict: """创建支付订单,返回 { 'pay_url': 'https://...', 'order_no': 'xxx' }""" pass @abstractmethod def verify_notify(self, data: dict) -> bool: """验证支付平台回调签名,返回 True/False""" pass @abstractmethod def query_order(self, order_no: str) -> dict: """查询订单状态,返回 { 'status': 'paid' | 'pending' | 'failed' }""" pass

你要做的,是新建payment/alipay.py(或payment/wechat.py),继承PaymentGateway并实现三个方法:

# app/src/payment/alipay.py from payment.base import PaymentGateway from alipay import AliPay # pip install python-alipay-sdk class AlipayGateway(PaymentGateway): def __init__(self): self.alipay = AliPay( appid="your_appid_here", app_notify_url="https://mall.example.com/api/payment/alipay/notify/", app_private_key_path="/var/www/mall/conf/alipay/private_key.pem", alipay_public_key_path="/var/www/mall/conf/alipay/alipay_public_key.pem", ) def create_order(self, order_id, amount, subject): order_string = self.alipay.api_alipay_trade_page_pay( out_trade_no=order_id, total_amount=str(amount / 100), # 分转元 subject=subject, return_url="https://mall.example.com/payment/success/", ) return { "pay_url": f"https://openapi.alipay.com/gateway.do?{order_string}", "order_no": order_id } def verify_notify(self, data): # 支付宝回调验签 signature = data.pop("sign") return self.alipay.verify(data, signature) def query_order(self, order_no): result = self.alipay.api_alipay_trade_query(out_trade_no=order_no) if result.get("trade_status") == "TRADE_SUCCESS": return {"status": "paid"} return {"status": "pending"}

然后在settings_prod.py中指定使用该网关:

# app/src/mall/settings_prod.py PAYMENT_GATEWAY_CLASS = 'payment.alipay.AlipayGateway'

逻辑说明:PAYMENT_GATEWAY_CLASS是一个字符串路径,Django 启动时通过import_string()动态导入类。这样,你更换支付渠道时,只需改一行配置,无需修改views.py中的调用逻辑。alipay_public_key.pem等密钥文件应放在conf/目录下,由supervisor启动时确保www-data可读(sudo chmod 644 /var/www/mall/conf/alipay/*.pem)。

5.2 短信服务接入:通过信号(Signal)解耦,零侵入修改

订单创建成功后发短信,传统做法是在OrderViewSet.create()里硬编码调用短信 SDK,导致测试困难、更换服务商成本高。本项目采用 Django Signal:

# app/src/orders/signals.py from django.dispatch import Signal # 定义信号 order_paid = Signal() # app/src/orders/views.py from django.dispatch import receiver from .signals import order_paid @receiver(order_paid) def send_sms_on_payment(sender, order_id, **kwargs): """收到 order_paid 信号后,调用短信服务""" from utils.sms import send_sms order = Order.objects.get(id=order_id) send_sms( phone=order.user.phone, template="您的订单 {order_no} 已支付成功,预计 {days} 天送达。", context={"order_no": order.order_no, "days": 3} )

你只需实现utils/sms.py:

# app/src/utils/sms.py import requests def send_sms(phone: str, template: str, context: dict): """对接云通讯平台(如某运营商 API)""" api_url = "https://sms-api.example.com/send" payload = { "phone": phone, "template": template, "context": context, "apikey": "your_apikey_here" # 从环境变量读取更安全 } response = requests.post(api_url, json=payload, timeout=10) if response.status_code != 200: raise Exception(f"SMS send failed: {response.text}")

最后,在订单支付成功的视图中触发信号:

# app/src/payment/views.py from orders.signals import order_paid def alipay_notify(request): # ... 验证回调逻辑 if verify_result: order = Order.objects.get(order_no=data['out_trade_no']) order.status = 'paid' order.save() order_paid.send(sender=Order, order_id=order.id) # ← 关键:触发信号 return HttpResponse("success")

这种设计的好处是:添加新通知方式(如邮件、站内信、Webhook)只需写一个新的@receiver函数,完全不碰原有支付逻辑。从那以后我每次接入第三方服务,都强制走一遍「定义信号 → 实现处理函数 → 触发信号」的三步,再复杂的业务流也能保持主干干净。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进