Linux下Django商城部署全链路解析与避坑指南
简介面向希望在 Linux 环境部署 Django 电商项目的开发者这份源码包提供了一套可运行的大型前后端商城基础架构重点覆盖框架核心逻辑与部署流程未集成支付宝和短信接口更适合聚焦业务实现、正在做课程设计或自主练习的初中级开发者。压缩包共 204 个文件、约 1.69MB包含 99 个 Python 源文件、30 个 HTML 模板、18 个 JS 脚本以及 CSS、字体、图片等静态资源其中 py 负责后端业务、html 用于页面渲染、js/css 完善前端交互还附带 SQL 数据库脚本与 Nginx 配置目录结构清晰适合按模块对照学习。目前已有 230 人学习下载。围绕商品、订单、用户认证等电商核心模块资源展示了 Linux 下的环境配置、数据库选型、视图与模板配合、用户权限控制以及后续部署优化等关键环节能帮助读者快速理解 Django 的 MVC 组织方式并掌握大型商城项目从开发到上线的基本套路。1. Linux下部署Django商城先理清链路再动手开发机上跑得好好的 Django 商城项目一部署到 Linux 服务器就各种翻车静态文件 404、上传的图片打不开、接口超时、内存告警。这不是代码问题是部署链路没理清。linux django 部署不是把代码传上去跑 runserver 就完事它涉及 Python 环境、数据库、应用服务器、反向代理、守护进程五层配合。这篇笔记适合谁刚把商城项目写完准备上线的开发者以及被部署折磨过的前后端工程师。我会把每一层的选型理由、参数设置和踩坑记录拆开讲照着做能少走两三天弯路。2. 环境选型与 Python 运行环境版本锁定是第一道坎2.1 为什么不能用系统自带 Python版本与 pip 的隐藏冲突很多 Linux 发行版自带的 Python 版本偏老某个发行版自带 2.7某个自带 3.8而新一点的商城项目依赖的 Django 版本对 Python 有最低版本要求跑起来直接报语法错误或依赖编译失败。更麻烦的是直接用系统 Python 装包会把 site-packages 目录搞脏后面再装别的项目依赖时互相覆盖排查起来像在解一团乱麻。常见做法是装一个干净的 Python 版本再用 venv 虚拟环境把商城项目的依赖隔离到独立目录。这样系统自带的 Python 不管怎么动都不影响项目运行。我先装 Python 3.10 和 venv 模块然后为项目建独立虚拟环境# 以某 Debian 系发行版为例安装 Python 3.10 与 venv 模块 sudo apt update sudo apt install -y python3.10 python3.10-venv python3-pip # 为商城项目创建独立虚拟环境/opt/shop 是项目根目录 python3.10 -m venv /opt/shop/venv # 激活后确认解释器路径避免误用系统 python source /opt/shop/venv/bin/activate which python python --version第一段命令安装的是 Python 解释器和 venv 标准库模块第二段创建的虚拟环境会把所有第三方包装进 /opt/shop/venv/lib/python3.10/site-packages不会动系统目录。激活后执行which python能看到路径指向虚拟环境这点很重要很多人部署时忘了激活后面 pip 装的包和跑项目的解释器不是同一个功能全对但就是报 ModuleNotFoundError属于最常见的玄学问题之一。2.2 依赖锁定pip freeze 的脏数据与 pip-tools 的用法商城项目依赖的包往往有几十个直接pip freeze requirements.txt会把当前环境里所有包全部锁进去包括你本机装过但与商城无关的工具包。这些多余包在内网服务器上可能根本装不上或者版本互相冲突最后线上环境跟开发环境不一致跑出诡异行为。我一般用 pip-tools 做依赖锁定从顶层依赖出发编译出完整锁定文件。先在项目里维护一份 requirements.in只写商城项目直接依赖的包版本用兼容区间不写死。再用 pip-compile 生成完全锁定的 requirements.txt最后在服务器上按锁定文件安装# requirements.in 内容示例 django4.2,5.1 mysqlclient2.2,3 redis5,6 # 在本机编译出锁定文件锁定所有传递依赖的精确版本 pip-compile requirements.in --output-file requirements.txt # 在服务器虚拟环境里按锁定文件安装 pip install -r requirements.txtpip-compile会解析每层依赖并写出精确版本号还会标注每个包被谁依赖后续升级某个主版本时能一眼看出影响范围。requirements.txt 第一行通常有--index-url注释内网部署时把它改成内部镜像源即可。注意 requirements.in 里版本区间不要写太宽Django 主版本升级带来的 ORM 行为变化可能让商城订单查询逻辑出问题大版本跳跃我一般会单独开分支验证后再合并。2.3 离线安装与 pip check没有公网时怎么装依赖内网服务器装依赖是另一个常见场景。服务器连不上公网直接 pip install 会卡在连接超时。做法是在有网的机器上用 pip download 把依赖包下载到目录拷到内网后用 --find-links 离线安装。这里有个坑下载时要指定平台参数否则可能下载到源码包或错误平台的二进制包。# 在有网的机器上下载依赖指定兼容 manylinux 的 x86_64 平台 pip download -r requirements.txt -d /opt/packages/ \ --platform manylinux2014_x86_64 \ --only-binary:all: # 拷贝到内网服务器后离线安装--no-index 禁止访问公网源 pip install --no-index --find-links/opt/packages/ -r requirements.txt # 最后校验所有包的依赖关系是否完整 pip check--only-binary:all:强制只下载预编译的 wheel 包避免内网机器缺编译工具链导致源码包安装失败如果你的服务器架构是 ARM要把平台参数换成对应的 manylinux aarch64。装完务必执行pip check它会逐一核对已安装包的依赖声明报出缺失或版本冲突。这个命令经常被忽略但部署商城这种百级依赖项目时它能在五分钟内帮你排除掉一半的环境类疑难杂症。3. 数据库与配置分离从 SQLite 切到 MySQL 的迁移方案3.1 数据库选型与字符集utf8mb4 和排序规则的坑商城项目的商品、订单、用户表都有大量并发读写开发环境用的 SQLite 撑不住生产流量必须切到独立的数据库服务。常见选择是 MySQL 8.0稳定且运维资料多。建库时字符集必须用 utf8mb4而不是 utf8因为 utf8 在 MySQL 里最多存 3 字节商品标题里一旦出现 emoji 或生僻字就存不进去报错你根本想不到是字符集问题。-- 建库指定字符集与排序规则utf8mb4_unicode_ci 兼容性最好 CREATE DATABASE shop_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 建应用账号只给必要权限生产库千万不要用 root 跑业务 CREATE USER shop_userlocalhost IDENTIFIED BY 强密码; GRANT ALL PRIVILEGES ON shop_db.* TO shop_userlocalhost; FLUSH PRIVILEGES;注意 MySQL 8.0 默认排序规则是 utf8mb4_0900_ai_ci如果后续要把库迁移回 5.7 或对接老系统这个规则不兼容建议建库时显式指定 utf8mb4_unicode_ci。另外shop_userlocalhost表示只允许从本机连接如果 Django 和数据库分机器部署要将 localhost 换成应用服务器的 IP否则应用连库时报 access denied会误导你去查密码对错。3.2 settings.py 生产配置环境变量统一管理与 DEBUGFalse开发时 settings.py 里写死 SECRET_KEY、数据库密码没问题但部署到服务器后这些配置绝不能留在代码里。一旦代码仓库泄露数据库密码和密钥全暴露。我习惯在项目里加一个 .env 文件由 systemd 或启动脚本注入环境变量settings.py 只从os.environ里取值没有硬编码。# settings.py 生产片段 import os from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent # 所有敏感配置走环境变量缺失直接抛异常避免带病启动 SECRET_KEY os.environ[DJANGO_SECRET_KEY] DEBUG os.environ.get(DJANGO_DEBUG, false).lower() true ALLOWED_HOSTS os.environ.get(DJANGO_ALLOWED_HOSTS, ).split(,) DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: os.environ.get(DB_NAME, shop_db), USER: os.environ.get(DB_USER, shop_user), PASSWORD: os.environ.get(DB_PASSWORD, ), HOST: os.environ.get(DB_HOST, 127.0.0.1), PORT: os.environ.get(DB_PORT, 3306), # 连接层字符集必须带否则读写中文和 emoji 可能乱码 OPTIONS: {charset: utf8mb4}, } } STATIC_URL /static/ STATIC_ROOT /opt/shop/static MEDIA_URL /media/ MEDIA_ROOT /opt/shop/mediaDEBUG 在线上必须为 False否则浏览器访问任何不存在的 URL 都会把完整报错栈打到页面上等于把服务器目录结构、代码行号、依赖版本全暴露给攻击者。ALLOWED_HOSTS 用逗号分隔从环境变量读入把商城域名加进去比如 shop.example.com。STATIC_ROOT 和 MEDIA_ROOT 需要指向服务器上的绝对路径Nginx 会从这里读文件后面第 4 章细讲。3.3 数据迁移dumpdata/loaddata 的边界与 makemigrations 顺序从 SQLite 切到 MySQL除了改数据库连接还要处理两个层面的迁移表结构迁移和业务数据迁移。表结构迁移靠 Django 自己的迁移体系顺序很重要。先确认模型定义能生成合法迁移文件再看迁移计划最后执行# 只检查模型与迁移文件是否一致不生成新文件 python manage.py makemigrations --check --dry-run # 预览将要执行的 SQL 语句确认没有意外的表删除 python manage.py migrate --plan # 正式执行迁移 python manage.py migrate--check --dry-run在无模型变更时返回退出码 0有变更返回非 0适合写进发布脚本做前置检查。--plan会列出所有待执行迁移及每个迁移的依赖顺序重点看有没有反向迁移或删表操作商城项目的订单表、支付记录表一旦被误删没有后悔药。业务数据迁移用 Django 自带的 dumpdata/loaddata 就够但有两个边界要清楚一是 contenttypes 和 auth.Permission 的表数据容易因跨环境 ID 冲突导出时排除掉二是大表数据在几百 MB 以上时dumpdata 性能很差这种情况建议直接用数据库工具做物理导出导入。# 导出业务数据排除框架内部表避免环境 ID 冲突 python manage.py dumpdata --natural-foreign --natural-primary \ -e contenttypes -e auth.Permission -o backup.json # 在新库里导入 python manage.py loaddata backup.json--natural-foreign和--natural-primary会把外键和主键转成自然键而非数据库自增 ID这样两个环境的记录 ID 不一致时也能正确关联。经验是订单、支付这类业务表别用这种方式反复导入导出多次 loaddata 可能产生重复记录生产环境数据迁移建议在停服窗口做并先备份目标库。3.4 静态文件收集与媒体目录权限collectstatic 的三件事Django 开发时静态文件由 runserver 自动处理部署后 DEBUG 关闭Django 不再托管静态文件需要先把所有静态文件收集到一个目录再交给 Nginx 服务。collectstatic 命令就是干这个的它会按 app 的 static 目录和 STATICFILES_DIRS 配置把文件复制到 STATIC_ROOT。媒体文件是运行时上传的商品图、头像不归 collectstatic 管但要提前建目录并给对权限。# 收集静态文件--noinput 跳过交互确认适合发布脚本 python manage.py collectstatic --noinput # 创建媒体目录并授权给运行用户 mkdir -p /opt/shop/media chown -R www-data:www-data /opt/shop/media chmod 750 /opt/shop/media这里为什么用 www-data 这个用户因为 Nginx 通常以 www-data 运行Django 上传的图片落在 /opt/shop/media 下Nginx 要能读到这个目录才能把图片返回给浏览器。如果目录属主是 root上传功能正常但访问图片时永远 403这就是典型的权限坑。collectstatic 收集到的静态文件在 /opt/shop/static 下同样要确认 Nginx 进程有读权限静态文件目录一般不需要写权限设为 755 即可。4. 应用服务器与反向代理Gunicorn Nginx 的组合4.1 Gunicorn 还是 uWSGIworker 类型与并发参数怎么定Django 自带 runserver 不支持并发生产请求必须用应用服务器。商城项目常见的选型是 Gunicorn 或 uWSGIGunicorn 配置简单、调试直观纯 Python 应用用它足够uWSGI 能做到的细粒度调优更多但配置复杂度也上来了部署维护成本高。我一般项目先用 Gunicorn真遇到性能瓶颈再考虑替换不在一开始把架构搞复杂。Gunicorn 的 worker 数量和类型直接决定并发能力和内存占用。sync worker 每个 worker 同时只能处理一个请求简单但占用高gthread worker 用线程池处理并发内存占用更低适合商城这种大量 IO 密集型请求。worker 数量的经验公式是 CPU 核数乘以 2 加 1但服务器内存有限时不能照搬需要按内存反推参数建议值说明workers32 核 4G 内存的服务器建议 3 个 workerworker-classgthread线程模型比 sync 更省内存threads4每个 worker 4 个线程峰值并发约 12timeout120请求超时秒数商城支付回调要放宽max-requests1000处理满 1000 个请求后重启 worker防内存泄漏# 启动 Gunicorn监听本机 8000 端口等待 Nginx 转发 gunicorn shop.wsgi:application \ --workers 3 \ --worker-class gthread \ --threads 4 \ --bind 127.0.0.1:8000 \ --timeout 120 \ --max-requests 1000 \ --max-requests-jitter 100 \ --access-logfile - \ --error-logfile -timeout 是一个容易被忽略的参数商城下单流程里如果包含外部支付回调或短信发送同步请求可能超过默认 30 秒Gunicorn 会把还没完成的请求掐断前端收到 502。max-requests 加 jitter 是为了防止多个 worker 同时达到上限集体重启造成服务抖动jitter 随机偏移让重启动作错开。bind 绑 127.0.0.1 而不是 0.0.0.0是为了让 Nginx 和应用走内网回环不把应用端口直接暴露到公网。4.2 Nginx 配置反向代理与静态文件分离Nginx 在部署链路里承担三层职责接收公网请求转发给 Gunicorn、直接服务静态文件和媒体文件、挂载 TLS 证书。商城项目的 Nginx 配置里静态文件走 alias 指到 STATIC_ROOT媒体文件走 alias 指到 MEDIA_ROOT其他动态请求才转发给 Gunicorn。如果静态文件也走反向代理Nginx 每次都要把请求转发给 Django性能浪费至少一个数量级。server { listen 80; server_name shop.example.com; # 商城上传商品图可能超过默认 1M这里放宽到 20M client_max_body_size 20m; # 静态文件由 Nginx 直接返回7 天浏览器缓存 location /static/ { alias /opt/shop/static/; expires 7d; access_log off; } # 商品图片所在目录Nginx 直接返回 location /media/ { alias /opt/shop/media/; } # 其余请求转发给 Gunicorn 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_connect_timeout 30; proxy_read_timeout 120; } }alias /opt/shop/static/;结尾的斜杠不能丢否则 URL 拼接时会多出一层路径。proxy_set_header Host $host;这行决定了 Django 收到的 Host 头是什么ALLOWED_HOSTS 校验和 CSRF 校验都依赖这个头漏掉这行你会看到后台登录一直报 CSRF 错误。X-Forwarded-Proto告诉 Django 原始请求是 http 还是 https配合后面第 6 章的安全配置使用。client_max_body_size 根据商城业务调整涉及商品多图上传时 2M 默认值太小但也不要盲目调大到 1G会占用大量 Nginx 和 Gunicorn 内存。4.3 systemd 守护崩溃自动重启与日志轮转Gunicorn 进程不能靠 nohup 裸跑服务器重启或 worker 崩溃时没人拉起服务商城就静默挂掉了。标准做法是写一个 systemd 服务单元让系统管理生命周期加上崩溃自动重启策略。服务单元的核心是 ExecStart 和环境变量注入EnvironmentFile 指向 .env 文件这样 settings.py 里读 os.environ 的配置都能生效。# /etc/systemd/system/shop.service [Unit] DescriptionDjango Shop Service Afternetwork.target mysql.service [Service] Userwww-data Groupwww-data WorkingDirectory/opt/shop # 从 .env 文件读入环境变量目录需保证 systemd 可以访问 EnvironmentFile/opt/shop/.env ExecStart/opt/shop/venv/bin/gunicorn shop.wsgi:application \ --workers 3 --worker-class gthread --threads 4 \ --bind 127.0.0.1:8000 --timeout 120 \ --max-requests 1000 --max-requests-jitter 100 Restartalways RestartSec5 [Install] WantedBymulti-user.target# 重新加载 systemd 配置启用并启动服务 sudo systemctl daemon-reload sudo systemctl enable --now shop # 查看运行状态和最近日志 sudo systemctl status shop sudo journalctl -u shop -n 100 --no-pagerRestartalways让服务无论什么原因退出都会自动拉起RestartSec5设置重启间隔避免进程崩溃后疯狂重启占满 CPU。Aftermysql.service声明依赖关系服务器开机时先起数据库再起应用。日志统一由 journald 收集排查问题时journalctl -u shop -n 100看最近 100 行即可不用去翻文件。注意 EnvironmentFile 的权限.env 里是数据库密码和 SECRET_KEY建议设为 600 属主 root避免其他系统用户读到。5. 部署避坑五类高频故障的现象、原因与解决5.1 静态文件 404页面有内容但彻底没样式现象商城首页能打开但 HTML 里引用的 CSS、JS 全部 404页面像上世纪网页后台管理界面完全错乱。原因基本就三个一是部署后忘了跑collectstaticSTATIC_ROOT 目录是空的二是 DEBUGFalse 后 Django 不再自动服务静态文件请求被转发到 Gunicorn 时 Django 直接返回 404三是 Nginx 的 alias 路径跟 STATIC_ROOT 配置不一致比如配置里写 /opt/shop/static 而实际文件在 /opt/shop/staticfiles。解决先确认静态文件确实收集到位再验证 Nginx 路径。我一般按顺序排查python manage.py collectstatic --noinput ls /opt/shop/static/ | head curl -I http://127.0.0.1:8000/static/app/css/main.css sudo nginx -tls看文件是否真实存在curl -I带返回头测试直接访问 Gunicornnginx -t检查配置语法。如果三者都正常但浏览器仍然 404检查 Nginx error.log 里的路径拼写重点看 alias 末尾斜杠这是 Nginx 配置里最容易翻车的位置。5.2 上传图片打不开商户传图成功但前端访问 403现象后台商品管理里上传商品图提示成功前台商品详情页图片裂开直接访问图片 URL 返回 403 或 404。原因403 通常是媒体目录权限问题Nginx 进程没有读权限404 则是 MEDIA_URL 和 Nginx location 没对上。另一个隐藏原因是 SELinux 开启的发行版上Nginx 被强制访问控制拦截即使文件权限全对也读不了。解决先调权限再看 SELinux 审计日志。sudo chown -R www-data:www-data /opt/shop/media sudo chmod 750 /opt/shop/media # 查看 SELinux 是否拦截了 Nginx 读取 sudo ausearch -m avc -ts recent | grep nginx如果权限和属主都正确且看到 AVC 拒绝记录用下面的命令放行或者把 SELinux 调到宽松模式但要谨慎sudo setsebool -P httpd_read_user_content 1上传成功但读不到是因为 Django 进程写文件时用的用户有写权限而 Nginx 读取时用的用户没有对应权限。这就是为什么前面强调要统一属主为 www-data不止是媒体目录上传过程中临时文件所在的目录也要检查。5.3 ALLOWED_HOSTS 或 CSRF 验证失败线上后台登录直接被拒现象商城前台接口偶尔返回 400 Bad Request后台管理登录提交时提示 CSRF 验证失败刷新页面再试偶尔能过稳定复现时让人抓狂。原因400 是 Django 的 ALLOWED_HOSTS 校验拒绝了 Host 头你没把线上域名加进白名单CSRF 失败是 Nginx 转发时丢了 Host 头或者站点启用了 HTTPS而 Django 的 CSRF_TRUSTED_ORIGINS 里还是 http。这两个问题表象不同根源都在反向代理层的头信息传递。解决先确认 Nginx 的 proxy_set_header 完整再同步管理端配置。proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme;然后在 settings.py 里加上 HTTPS 来源ALLOWED_HOSTS [shop.example.com] CSRF_TRUSTED_ORIGINS [https://shop.example.com]如果已经启用 HTTPSCSRF_TRUSTED_ORIGINS 里的协议必须写成 https否则 Django 校验 Origin 头时比对失败现象就是登录表单永远报 CSRF 错误。这个错误信息往往把注意力往 cookie 上引实际上就是来源协议不匹配属于部署层最容易被误判的一类问题。5.4 OOM服务器内存告急Gunicorn worker 被内核杀掉现象白天流量正常晚上数据抓取任务一跑服务器开始卡顿systemctl status 显示 Gunicorn 服务不断重启dmesg 里有 Killed process 记录。原因Gunicorn 的 worker 数量配多了。每个 worker 的虚拟内存可能占到几百 MB3 个 worker 加系统其他进程4G 内存的服务器很容易触发内核 OOM killer。另一个次因是商城后台导出报表或跑对账任务时一次性加载大量对象到内存。解决先看当前内存占用和日志再按内存反推 worker 数。free -h sudo journalctl -k | grep -i oom ps aux --sort-%mem | head -n 102G 内存的服务器建议 workers 配 2、threads 配 4同时给系统加 2G swap 作为缓冲。另外在 Gunicorn 启动参数里保留--max-requests 1000可以定期回收泄漏内存。如果商城有定时任务考虑把任务拆到单独的 worker 进程和应用服务隔离避免集中跑任务时拖垮主服务。5.5 时区与中文乱码订单时间差 8 小时商品名变问号现象订单创建时间显示比实际慢 8 小时数据库中存的中文商品名变成一堆问号日志里还有 Incorrect string value 报错。原因时间问题通常是 settings.py 没设 TIME_ZONE默认用了 UTC中文乱码是数据库表字符集不是 utf8mb4或 Django 连接数据库时没带 charset 参数。前者好理解后者隐蔽在多处建库时字符集对但表是从旧库导入的遗留表还是 latin1。解决统一配置时区并修复表和连接两层的字符集。# settings.py LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ True-- 把剩余遗留表转成 utf8mb4 ALTER TABLE shop_sku CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;排查字符集时先确认 Django 连接参数里带了 charset再查表字符集顺序不能反。如果连接层已经带了 utf8mb4 而表还是 latin1写入时数据已经按 utf8mb4 编码进入 latin1 列转表前最好先备份转换后逐个确认数据无乱码这个操作在生产环境要放在低峰窗口做并经过完整回归。6. 上线前的压测与安全基操先让自己跑一遍生产流程部署完成不等于能上线我见过太多项目在测试环境通的一上真实流量就崩。商城这种重读写的业务上线前至少要做一次基础压测并过一遍安全自检。压测工具用 ab 就够不用上复杂工具安全自检就查三个最容易漏的配置。先压测一个高频接口比如商品列表接口或购物车接口。ab 的并发参数要低于 Gunicorn 实际承载能力不然压测本身就制造了大量 502ab -n 1000 -c 50 http://127.0.0.1:8000/api/products/看三个指标Failed requests 必须为 0Requests per second 在 2 核 4G 机器上稳定过百就算合格Time per request 不要超过 300 毫秒。如果 Failed requests 不为 0优先查 Gunicorn error log 和 Nginx error log多半是 timeout 或 worker 数不够。安全自检方面第一确认 DEBUGFalse 且 SECRET_KEY 换成了新生成的随机值第二用 curl 看响应头确认没有暴露 Django 版本信息并加上基本安全头第三确认 HTTPS 已经配置。响应头检查命令很简单curl -I https://shop.example.com/检查响应头里是否有 Strict-Transport-Security、X-Content-Type-Options 等字段。没有的话在 Nginx server 块里加add_header Strict-Transport-Security max-age31536000 always; add_header X-Content-Type-Options nosniff; add_header X-Frame-Options DENY;HSTS 头只应在 HTTPS 完全配置好后开启否则会强制浏览器走 https 导致访问失败。这一步做完商城部署链路基本闭环代码上线、静态资源分离、数据库可靠、进程托管、压测通过、安全头到位。从那以后我每次部署完商城项目都强制自己走一遍压测加响应头检查逐项核对才敢把流量切过去。这条流程帮我挡掉过好几次线上事故希望帮到你。本文还有配套的精品资源点击获取