☰
Linux部署Django大型商城:从环境规划到Gunicorn与Nginx完整实战
2026/10/13 3:55:43 网站建设 项目流程

简介:这是一套Linux部署环境下Django大型前后端商城项目的完整源码包,适合具备Python与Web开发基础的工程师,用于学习Django框架在电商领域的实际落地方式;项目未集成支付宝与短信接口,专注呈现商城核心业务逻辑与生产部署架构,便于集中精力理解主体链路。压缩包共204个文件,体积仅1.69MB,其中99个Python源码文件承载模型、视图与业务处理逻辑,30个HTML模板配合18个JavaScript脚本和3个CSS样式表构成前端页面,另有SQL、配置文件与多张图片素材,可直接对照学习前后端交互方式。目前已有230人学习下载。资源以manage.py、shopping_mall主应用和requirements依赖清单构成标准Django工程,覆盖Linux环境配置、MySQL数据库接入、商品与订单模型设计、URL与视图映射、模板渲染、用户认证、Redis缓存、Celery异步任务以及Gunicorn与Nginx部署等完整链路,目录结构清晰,适合按模块逐步研读,帮助开发者快速搭建并理解一个可用商城系统的整体架构。

1. linux部署django大型前后端商城,卡在部署链路而不是业务代码

在linux部署环境下把django大型前后端商城完整跑起来,真正难的不是商品模块或购物车逻辑,而是整个部署链路:Python环境、MySQL连接、Gunicorn进程、Nginx反向代理、静态资源和媒体文件分流,任何一环都能让服务在上线前一小时突然翻车。我见过不少项目本地runserver一切正常,push到服务器后图片404、接口502、Django重启起不来,最后只能对着日志排查到后半夜。这篇文章会把一套我反复用过的部署方案完整拆开讲清,从环境规划和账号开始,一路到settings改造、Gunicorn托管、Nginx静态资源伺服,最后是高频坑的排查方法。适合有Django开发基础、正准备把商城项目放到linux服务器的开发者和中小团队。

2. linux部署前的环境规划:账号、Python虚拟环境与中间件选型

2.1 先定运行账号和目录结构:为什么不要用root跑商城

很多人在linux服务器上做第一版部署时,习惯直接在root下pip install、python manage.py runserver,最后商城确实能打开,但隐患很大:Django进程一旦被利用,攻击者拿到的是root shell;后来想换目录、改权限也会被root创建的只读文件卡住。常见做法是先建一个专用系统账号,再把代码、虚拟环境、日志、上传文件分目录放。

sudo useradd --system --home /srv/shop_demo --shell /usr/sbin/nologin shopuser sudo mkdir -p /srv/shop_demo/media /srv/shop_demo/static sudo mkdir -p /srv/shop_demo/logs /srv/shop_demo/run sudo chown -R shopuser:shopuser /srv/shop_demo

这里--system表示系统账号,不会出现在普通登录列表中;--shell /usr/sbin/nologin让这个账号无法交互登录,即使Gunicorn被攻破,shell权限也受限。目录拆分后,media放用户上传的商品图、头像,static放collectstatic后的Django静态资源,logs给Gunicorn和Nginx日志,run放pid文件。日常部署我一般用sudo -u shopuser bash -c '命令'切换身份执行,而不是直接su,因为nologin账号su进去会被拒。

下一步需要把git仓库代码放进/srv/shop_demo,注意代码目录所有者必须是shopuser。如果代码是用root clone的,之后Gunicorn写日志、写入media都会报权限错误。遇到这类问题先回头看目录所有权,这是我被问过最多的问题之一。

2.2 用python3 venv隔离商城依赖:创建命令与依赖全流程

Django商城项目依赖很杂:Django、DRF、mysqlclient、django-redis、django-cors-headers、Celery也可能有。如果直接装在系统Python里,很容易和系统包冲突,升级一个库搞崩另一个服务。linux部署下我基本只用python3 -m venv做隔离,不用虚拟环境管理工具额外折腾。

sudo apt-get update sudo apt-get install -y python3-venv python3-dev default-libmysqlclient-dev build-essential pkg-config sudo -u shopuser bash -c 'cd /srv/shop_demo && python3 -m venv venv && source venv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt'

python3-dev提供Python头文件,default-libmysqlclient-dev是mysqlclient编译时需要的客户端开发库。如果缺这两个,pip install mysqlclient会在编译阶段报mysql_config not found。build-essential和pkg-config同样是为了本地编译Python扩展。项目依赖下的requirements.txt里我一般会锁版本,不锁版本号的话,三个月后同一份requirements可能装出一套完全不同的依赖组合。

安装完成以后验证一下:/srv/shop_demo/venv/bin/python -c "import django, mysqlclient",能正常输出就说明环境可用。注意这里每次操作都要用绝对路径/srv/shop_demo/venv/bin/python,不要在systemd或Nginx里依赖source activate,那些环境默认shell不加载venv,后面的坑基本都从这里来。

2.3 中间件选型:MySQL、Redis与异步队列到底装什么

大型商城不等于每个组件都要上。我见到的失败案例是照着大厂架构图把MySQL、Redis、RabbitMQ、Elasticsearch全部装一遍,结果单机资源全被中间件吃掉,Django反而跑不动。选型要看现阶段真实规模。下面是我在初期商城项目里常用的判断表:

中间件商城里的角色初期要不要装部署注意点
MySQL商品、订单、用户、库存持久化必须设utf8mb4,调大innodb_buffer_pool_size
Redis商品缓存、购物车、分布式锁、Celery broker必须开appendonly,设maxmemory防止占满内存
RabbitMQ订单超时、短信、支付回调等可靠异步消息不急着装先用Redis做broker,消息量大再拆
Elasticsearch商品搜索、筛选、分词看搜索复杂度MySQL里的LIKE会拖垮商品表,但数据量小时不用上

一键安装和启动命令很简单:sudo apt-get install -y mysql-server redis-server,然后systemctl enable --now mysql redis-server。真正要花时间的是配置:MySQL要新建商城专用库和账号,不能用root连Django;Redis默认监听127.0.0.1就够,不要暴露到公网。异步任务方面,我最常用的做法是先让Celery使用Redis作为broker,少维护一个RabbitMQ。等订单量起来、出现消息丢失或积压问题时,再替换成RabbitMQ,业务代码改动不大。

3. Django生产配置改造:settings拆分与静态媒体目录的四个关键调整

3.1 把settings.py拆成包:base、dev、prod各自管什么

商城项目里一个settings.py写到三四百行是常态,生产配置和开发配置混在一起,上线时临时关DEBUG、改ALLOWED_HOSTS,容易漏项。常见做法是把settings改成包结构,按环境拆开:

shop_demo/settings/ ├── __init__.py ├── base.py # 公共配置 ├── dev.py # 开发环境 └── prod.py # 生产环境

__init__.py保持空文件即可,关键是在启动命令里指定模块。manage.py里默认走dev.py,生产用Gunicorn时通过环境变量DJANGO_SETTINGS_MODULE=shop_demo.settings.prod切换。prod.py从base导入全部公共配置,只覆盖生产差异:

# prod.py import os from .base import * DEBUG = False ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "127.0.0.1").split(",") SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": os.environ["DB_NAME"], "USER": os.environ["DB_USER"], "PASSWORD": os.environ["DB_PASSWORD"], "HOST": os.environ.get("DB_HOST", "127.0.0.1"), "PORT": os.environ.get("DB_PORT", "3306"), "CONN_MAX_AGE": 60, "OPTIONS": {"charset": "utf8mb4"}, } } CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": os.environ.get("REDIS_URL", "redis://127.0.0.1:6379/1"), "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, } } MEDIA_URL = "/media/" MEDIA_ROOT = "/srv/shop_demo/media" STATIC_URL = "/static/" STATIC_ROOT = "/srv/shop_demo/static"

SECRET_KEY不能有默认值,启动时没设置就直接报错,这是故意的:防止有人把生产密钥写进代码库。ALLOWED_HOSTS从环境变量读取,避免每次改配置都要改文件。CONN_MAX_AGE=60让Django复用MySQL连接,减少握手次数,对商城这种请求量上来的场景很重要。utf8mb4保证商品标题里的emoji和生僻字不会变成问号,这个坑在商城评论场景特别常见。

3.2 生产环境数据库、缓存和静态媒体路径配置

配置写完后,第一次部署要按顺序执行迁移和静态文件收集。collectstatic对Django admin特别关键,很多商城的后台管理系统是Django自带admin,如果没收集静态资源,admin页面会没有CSS,长着一张“裸奔”的脸。

sudo -u shopuser bash -c 'cd /srv/shop_demo && source venv/bin/activate && python manage.py migrate --noinput && python manage.py collectstatic --noinput && python manage.py check --deploy --settings=shop_demo.settings.prod'

--noinput避免命令中途询问确认,适合部署脚本里执行。migrate是建表和数据迁移。check --deploy会按生产安全标准检查配置,常见提示包括SECRET_KEY是否明文、DEBUG是否关闭、是否启用安全中间件等。它不会直接帮你改,但会把问题列出来,一条条对照处理即可。

要注意collectstatic收集的是Django各类app里的静态资源,对前后端分离商城来说,前端Vue或React的构建产物不应该塞进STATIC_ROOT,它应该由Nginx直接伺服独立目录。否则前端构建一次,就要往Django静态目录拷一次,职责混乱。媒体文件是用户上传的商品图,根本不会出现在collectstatic结果里,所以MEDIA_ROOT要指向一个独立目录,后续迁移对象存储时整个目录搬走即可。

3.3 跨域与Cookie:前后端分离商城必须处理的CORS

前后端分离商城最常见的部署形态有两种。第一种是同一域名下用路径区分,例如https://shop.example.com/放前端页面,/api/走Django接口,这种同域部署不需要CORS。第二种是前端域名和API域名不同,比如https://shop.example.com对https://api.example.com,这种必须处理跨域。我建议小团队优先选同域部署,省掉CORS相关的所有问题;但如果你有独立客户端域名或未来要开放API,跨域配置也要会。

# prod.py 追加 INSTALLED_APPS += ["corsheaders"] MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware"] + MIDDLEWARE CORS_ALLOWED_ORIGINS = ["https://shop.example.com"] CORS_ALLOW_CREDENTIALS = True CSRF_TRUSTED_ORIGINS = ["https://shop.example.com"]

corsheaders要放在中间件最前面,因为跨域请求的预检OPTIONS要第一时间拿到响应头。CORS_ALLOW_CREDENTIALS=True表示允许带Cookie,前提是前端用withCredentials: true发起请求。CSRF_TRUSTED_ORIGINS是Django 4以后单独要求的配置,不加的话即使CORS通过,POST/PUT/DELETE也容易被CSRF校验拦下。如果用JWT且把token存在前端localStorage,Authorization头不依赖Cookie,可以不开Credentials,但商城登录态用HttpOnly Cookie更安全,所以这两项我一般同时配。

4. 用Gunicorn和systemd把Django商城进程管起来:worker数量与重启机制

4.1 为什么runserver不能上生产:Gunicorn的进程模型

python manage.py runserver是开发服务器,自带reload、单进程、性能很低,而且它默认绑定127.0.0.1,真要暴露公网也不安全。生产商城的Django应用需要一个WSGI服务器来承担并发请求,Gunicorn是比较通用的方案。它的模型是master管理多个worker进程,worker接收HTTP请求并调用Django的WSGI应用。

选择Gunicorn而不是uWSGI的考量是配置简单、日志干净,对Django生态支持好。uWSGI性能上限更高,但配置项复杂,大型团队愿意调优可以选;中小商城项目用Gunicorn的人更多。注意Gunicorn是同步worker模型,如果接口里有长时间阻塞操作,worker会被占住,后面的请求排队。所以商城里的导出、批量通知这类操作一定要丢给Celery异步执行,否则再多的worker也不够耗。

4.2 worker数量、并发类型和内存预算的计算方式

网上最常看到的公式是2 * CPU核数 + 1,它来自CPU密集型场景,但商城API大部分时间在等MySQL、Redis返回,属于IO密集型。如果只用sync worker,一个worker同时只能处理一个请求,MySQL查询一慢整个接口就排队。Gunicorn提供gthread类型,让每个worker内开多线程,更适合IO密集的Django商城。

cd /srv/shop_demo DJANGO_SETTINGS_MODULE=shop_demo.settings.prod \ /srv/shop_demo/venv/bin/gunicorn shop_demo.wsgi:application \ --bind 127.0.0.1:8000 \ --workers 4 \ --threads 4 \ --worker-class gthread \ --timeout 60 \ --graceful-timeout 30 \ --max-requests 2000 \ --max-requests-jitter 200 \ --access-logfile /srv/shop_demo/logs/gunicorn-access.log \ --error-logfile /srv/shop_demo/logs/gunicorn-error.log \ --pid /srv/shop_demo/run/gunicorn.pid

--worker-class gthread决定用线程并发;--threads 4表示每个worker内4个线程。worker数量不是越大越好,一个Django worker大概占用150MB到250MB内存,4个worker加线程开销差不多1GB。我常用的初始配置如下:

云主机配置建议workeersthreads大致并发能力适用阶段
2核4G24同时8个请求刚上线、流量不大
4核8G44同时16个请求常态业务
8核16G64同时24个请求活动期前置

--max-requests 2000和--max-requests-jitter 200的意思是worker每处理约2000个请求后自己重启,jitter随机偏移,防止多个worker同时到期同时重启造成服务空窗。这是对抗Python内存泄漏的后悔药,商城系统跑几天后内存缓慢上涨很常见,除非你有时间排查每个依赖,否则先用这个机制兜底。

4.3 systemd服务文件:开机自启、崩溃拉起和优雅重启

Gunicorn不能靠nohup后台挂了就不管,服务器重启、进程崩溃、worker泄漏都需要一个守护者。systemd是linux发行版自带的进程管理,配置好服务文件后可以让Gunicorn随机器启动、崩溃自动拉起、日志进journald。

; /etc/systemd/system/shop-gunicorn.service [Unit] Description=shop_demo gunicorn After=network-online.target mysql.service redis-server.service Wants=network-online.target [Service] Type=notify User=shopuser Group=shopuser WorkingDirectory=/srv/shop_demo EnvironmentFile=/etc/shop_demo/shop.env ExecStart=/srv/shop_demo/venv/bin/gunicorn shop_demo.wsgi:application \ --bind 127.0.0.1:8000 \ --worker-class gthread \ --workers 4 \ --threads 4 \ --timeout 60 \ --max-requests 2000 \ --max-requests-jitter 200 ExecReload=/bin/kill -s HUP $MAINPID Restart=always RestartSec=3 KillMode=mixed [Install] WantedBy=multi-user.target

After里面带上mysql和redis服务名,保证数据库和缓存在Gunicorn前先启动。Type=notify需要Gunicorn支持,master进程真正ready后systemd才认为服务启动成功,比固定sleep几秒更可靠。EnvironmentFile指向/etc/shop_demo/shop.env,里面用KEY=value格式存放所有敏感配置,这个文件权限要设为600。ExecReload发送HUP信号给master,worker优雅处理完手头请求后再退出,适合改配置后热加载。

启动命令如下:

sudo mkdir -p /etc/shop_demo sudo touch /etc/shop_demo/shop.env sudo chmod 600 /etc/shop_demo/shop.env sudo systemctl daemon-reload sudo systemctl enable --now shop-gunicorn systemctl status shop-gunicorn

如果启动失败,先看journalctl -u shop-gunicorn -e,不要反复restart。我见过最隐蔽的问题是EnvironmentFile权限不对,systemd直接拒绝服务启动;还有Type=notify配合旧版Gunicorn会出现启动超时,这时要么去掉notify改成simple,要么升级Gunicorn版本。

5. 常见问题排查:Nginx反向代理与前后端联调的5个坑

5.1 一套同时伺服前端静态页与Django API的Nginx配置

Nginx是整套部署的入口。它的核心任务是:把/api/和/admin/请求转给Gunicorn,把/static/和/media/直接用文件系统响应,然后剩下的路径全部交给前端页面。一个常见错误是直接用Django伺服前端dist,这既慢又不安全。下面是我常用的最简配置:

server { listen 80; server_name shop.example.com; client_max_body_size 20m; root /srv/shop_demo/frontend/dist; index index.html; gzip on; gzip_types text/css application/javascript application/json image/svg+xml; location /api/ { 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_connect_timeout 30s; proxy_read_timeout 60s; } location /admin/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; } location /media/ { alias /srv/shop_demo/media/; expires 30d; add_header Cache-Control "public, immutable"; } location /static/ { alias /srv/shop_demo/static/; expires 30d; } location / { try_files $uri $uri/ /index.html; } }

proxy_pass后不带斜杠,所以/api/order/传到Django时URI不变;如果写成http://127.0.0.1:8000/带斜杠,会把/api前缀剥掉,后端URL就必须去掉api前缀。商城API通常有统一前缀,所以我保留原样。try_files $uri $uri/ /index.html是给前端history路由用的,页面刷新时Nginx找不到对应路径,就回退到首页,由前端路由接管。这个配置下要注意location匹配规则,/api、/media、/static这些前缀优先级高于/,不会误落到前端回退逻辑。

5.2 现象502:Gunicorn没起来还是地址不对

现象:打开商城首页正常,点接口请求后返回502,Nginx错误日志里出现connect() failed (111: Connection refused) while connecting to upstream。

原因:Nginx把请求转发到127.0.0.1:8000,但那里没有进程在监听。常见是Gunicorn服务没启动、启动后崩溃、或者bind地址写成了0.0.0.0:9000。还有一个隐藏原因:systemd服务启动成功,但worker异常退出,master还没来得及拉起。

解决:先手动探测后端是否活着。执行curl -v http://127.0.0.1:8000/api/health/,如果Connection refused说明Gunicorn没在跑。接着systemctl status shop-gunicorn看进程状态,journalctl -u shop-gunicorn -e看崩溃日志。确认Gunicorn正常运行后,再检查Nginx里的proxy_pass地址、端口和后端bind保持一致。我自己踩过最蠢的一次是改了Gunicorn端口但没改Nginx,排查了半小时才想起来。

5.3 现象504:超时是表象,慢在接口还是链路上

现象:下单接口偶尔请求很久后返回504 Gateway Timeout,Nginx错误日志提示upstream timed out。

原因:Gunicorn的--timeout默认30秒,Nginx的proxy_read_timeout默认60秒。商城某个接口调外部支付网关或MySQL慢查询,耗时超过这两个阈值,Nginx直接断开连接。这时候调大超时只是掩耳盗铃,真正要解决的是接口为什么慢。

解决:先用日志定位。Gunicorn的access log里每行有响应时间,找到耗时超过1秒的接口,再看对应SQL和外部调用。如果是MySQL查询慢,加索引、减少N+1查询;如果是支付回调这类耗时操作,改成Celery异步处理,接口立刻返回“处理中”。超时参数本身建议设成:Gunicorn--timeout 60,Nginxproxy_read_timeout 120s,这是兜底不是优化。

5.4 媒体404、admin裸奔和跨域403:三个容易被忽略的配置点

现象1:商品图片上传成功,但页面访问/media/xxx.jpg返回404。原因是Nginx的alias路径拼接错了。location /media/对应alias /srv/shop_demo/media/;,如果alias少了末尾斜杠,请求/media/goods/a.jpg时会拼成srv/shop_demo/mediagoods/a.jpg。同样,Django里MEDIA_ROOT和Nginx alias的根目录必须指向同一个实际路径。

现象2:Django admin页面可以登录,但CSS和JS全部丢失。原因是生产环境没有执行collectstatic,或者Nginx的/static/指向了错误的目录。执行过一次collectstatic后,还要确认/srv/shop_demo/static/admin/目录存在。如果存在但样式还是丢了,大概率是Nginx的location匹配被前端回退规则覆盖,注意/static/的location要写在location /之前,Nginx取最长前缀匹配。

现象3:前端和API域名不同,POST请求返回403,浏览器控制台提示CSRF验证失败。原因是只配了CORS,没配CSRF_TRUSTED_ORIGINS。Django对带Cookie的跨域请求要求信任来源域名,所以要在prod.py里同时设置CORS_ALLOWED_ORIGINS和CSRF_TRUSTED_ORIGINS,而且必须写完整协议和域名,不能带路径。这是一个改一行就解决的配置,但它最容易让人怀疑人生,因为CORS响应头明明已经通过了。

6. 上线后的快速验证与性能调优技巧

6.1 用curl和Nginx日志找出隐藏慢接口

上线后第一件事不是打开浏览器点页面,而是对关键接口做一次带耗时统计的请求。下面这条命令会输出HTTP状态码和总耗时:

curl -s -o /dev/null -w "code:%{http_code} total:%{time_total}s\n" http://127.0.0.1:8000/api/health/

把地址换成商品列表、商品详情、加入购物车、提交订单这几个商城核心接口,逐个跑一遍。耗时超过500ms的接口要警惕。接着在Nginx配置里加一条自定义日志格式,把$request_time打进access log:

log_format timed '$remote_addr [$time_local] "$request" $status $request_time'; access_log /srv/shop_demo/logs/nginx-access.log timed;

重新加载Nginx后,运行一会儿,用awk '$NF > 1 {print}'筛出耗时大于1秒的请求,按URL聚合,就能知道商城目前最慢的接口是谁。不要靠浏览器F12感觉,线上真实流量下的耗时才准确。

6.2 Gunicorn平滑重载与真实流量压测

更新代码后需要重启Gunicorn。直接systemctl restart会中断所有worker,高峰时线上会出现几十个请求失败。正确做法是sudo systemctl reload shop-gunicorn,这条命令通过ExecReload向master发送HUP信号,worker处理完当前请求才退出,新代码在下一批请求加载。整理发布流程时把这一步固定下来,能避免很多“升级导致订单失败”的血泪经验。

压测时不要一上来就压Gunicorn,要先压完整Nginx链路。ab -n 2000 -c 50 https://shop.example.com/api/goods/可以粗略看吞吐和P95耗时。压测期间盯两个指标:worker内存是否持续上涨、MySQL连接数是否打满。如果连接数飙高,检查CONN_MAX_AGE是否生效;如果内存只涨不降,把--max-requests数值调小,让worker更快轮换。

6.3 MySQL连接和Redis缓存的两个调优点

商城部署稳定后,性能瓶颈通常会从代码转移到MySQL连接管理和缓存命中率。Django的CONN_MAX_AGE=60能复用连接,但MySQL默认wait_timeout可能是28800秒,Django的连接在空闲后会被MySQL主动断开,复用到一个已断开的连接时会出现MySQL server has gone away。常见做法是把映射关系调整成Connection寿命小于MySQL空闲阈值,或者在启动健康检查请求检测连接可用性。另一个取舍是Redis缓存过期策略:商品详情缓存15分钟、首页轮播缓存10分钟、库存变化实时失效。击穿和雪崩可以通过主动过期打散、热点数据永不过期加定时更新来缓解。

我印象最深的一次排障,是某个模拟商城项目上线前突然出现周期性的502,查了Gunicorn日志后发现worker每隔几百个请求就重启一次,重启窗口里的请求全部失败。原因就是--max-requests设置成500且没有jitter,所有worker在同一时间点集体轮换。调成2000加jitter后,问题再没出现过。从那以后我养成了一个习惯:每次部署上线至少观察一小时Gunicorn日志,重点看不正常重启和5xx状态码,确认稳定后才算真正收工。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询