1. 部署前先把架构想清楚:一条请求从浏览器走到服务器的完整路径
上手服务器之前,我最常见到的状态是:本地localhost:8000跑得好好的,Vue 的npm run dev也没问题,结果一放到服务器上,浏览器打开要么是空白页、要么是 502,半天不知道问题出在哪。做“服务器部署 Nginx + Django + Vue”这件事,最大的障碍从来不是某个配置不会写,而是脑子里没有完整的架构图。
先花三分钟把这条请求链路讲明白。一个用户从浏览器输入域名回车之后,请求先到达服务器上的 Nginx,Nginx 这时候做的事情是“分发员”:它会看请求的路径开头是什么,再决定把请求交给谁。如果路径是/api/,说明这是后端接口,Nginx 会把请求反向代理给 Django 应用服务器;如果路径是别的普通路径,比如/或者/about,Nginx 直接去磁盘上找 Vue 构建好的静态文件返回给浏览器。
把你本地开发时那套“两只服务同时跑”的思维扔到一边。生产环境里,Django 只专注处理业务逻辑和数据,Vue 只负责生成一堆静态的 HTML、JS、CSS 文件,Nginx 是唯一的对外门户。这样拆分之后,每一层的职责非常清楚,出了问题也能第一时间定位到是哪个环节。
这套架构里另一个容易忽略的点是:Django 的静态文件交给 Nginx 管。很多人第一次部署时,把所有东西都塞给 Django 的 runserver,结果上线后页面样式全丢。记住一句话:生产环境中,凡是纯静态资源(前端 JS、CSS、图片,Django 的 admin 后台样式),都不该由 Django 自己出,应该由 Nginx 直接读文件系统返回,性能能差一个数量级。
部署前的准备工作,我建议按这个顺序做:
- 确认服务器系统版本,我习惯用 Debian 系(Ubuntu 或 Debian),apt 装包最省心。
- Python 版本确认,Django 4.x/5.x 都要求 Python 3.10+,别在 3.8 上浪费时间。
- Node.js 版本,Vue 3 项目建议 Node 18 以上,装之前先
node -v。 - 准备好域名或者能用的公网 IP,后面 Nginx 配置里要用。
- 检查服务器端口是否放行,至少 80 端口要能访问。
如果你的服务器是云服务厂商的,还要注意安全组规则。我踩过一次:Nginx 配好了、Django 也起来了,外网就是连不上,最后发现是安全组没放行 80 端口。在本地curl http://127.0.0.1是通的,在服务器上curl http://公网IP就不通,这种低级错误最容易排查很久,因为你会下意识认为是配置写错了,实际上跟配置一点关系都没有。
2. 后端 Django 上服务器的正确姿势:虚拟环境、进程守护与 Uvicorn 选型
2.1 虚拟环境与依赖安装:别再用全局 Python 环境了
服务器上的 Python 环境非常宝贵,系统自带的 Python 可能被很多系统工具依赖,你贸然往全局环境里pip install一堆包,轻则污染系统,重则把系统工具搞挂。我见过有人为了装某个包把系统 Python 给升级了,最后服务器 SSH 登录都出问题的案例。正确的做法是给项目单独开一个虚拟环境,这是服务器上的底线操作。
登录服务器后,我会把项目代码放到/var/www/或者/opt/下面,目录结构建议这样规划:
/var/www/myproject/ ├── backend/ # Django 项目 │ ├── manage.py │ ├── myproject/ # Django 配置文件目录 │ └── venv/ # 虚拟环境 ├── frontend/ # Vue 项目源码 └── dist/ # Vue 构建产物(可以被 Nginx 直接使用)创建虚拟环境并安装依赖:
cd /var/www/myproject/backend python3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install django gunicorn uvicorn daphne依赖这块有两种人:一种人把所有包都pip freeze导出,我建议筛一筛,只保留项目实际用到的。如果项目用了 Django REST Framework,就加djangorestframework;用了 CORS 就加django-cors-headers;用了 ORM 连数据库,把对应的驱动也装上。要注意pip freeze导出的 requirements.txt 里可能有大量传递依赖,虽然不影响正确性,但装起来容易出版本问题。
2.2 关键配置修改:ALLOWED_HOSTS 和静态文件收集
Django 上线之前,至少改两个配置。第一个是settings.py里的ALLOWED_HOSTS。本地开发时这个列表通常是空的,意味着只允许 localhost 访问,生产环境如果你不把域名或 IP 加进去,Django 会直接拒绝请求,返回 400。具体这样改:
ALLOWED_HOSTS = ["your-domain.com", "你的服务器IP"] # 不要用 *,用 * 生产环境是巨大的安全隐患第二个是静态文件目录。STATIC_ROOT指的是执行collectstatic之后,所有静态文件集中存放的目录。我们没有给 Django 配什么特殊的路径,直接用一个普通目录:
STATIC_URL = "/static/" STATIC_ROOT = BASE_DIR / "staticfiles" MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media"然后执行:
source venv/bin/activate python manage.py collectstatic --noinput python manage.py migratecollectstatic会把 Django admin 自带的样式、你项目 static 目录下的所有文件都复制到staticfiles/下。执行之前记得先确认INSTALLED_APPS里django.contrib.admin是启用的,不然 admin 后台的 CSS 会收集不全。
2.3 启动方式选型:Gunicorn、Uvicorn 还是 Daphne?
这是新手最容易纠结的问题。我的建议很简单:Django 项目用的是普通视图、API 接口,选 Gunicorn 最稳妥;如果项目里用了 Django Channels 处理 WebSocket,那就用 Daphne 或 Uvicorn,因为它们能处理 ASGI 协议。
从部署实践的角度来说,纯 HTTP 的 Django 项目用 Gunicorn 作为 WSGI 服务器,配合gunicorn myproject.wsgi:application启动,这个组合是最成熟稳定的,文档多、踩坑的人也多,网上能搜到大量解决方案。如果你用了异步特性,选 Daphne 会需要写个asgi.py的入口,因为 Daphne 并不是从 Django 工厂函数直接拿 app 的。
我用表格给你对比一下:
| 服务器 | 协议 | 适用场景 | 生产环境推荐度 |
|---|---|---|---|
| Gunicorn | WSGI | 传统 Django 视图、DRF 接口、Celery 任务 | 高,稳定优先 |
| Uvicorn | ASGI | 用了 async 视图、FastAPI 迁移、WebSocket | 中高,配合 --workers |
| Daphne | ASGI | Django Channels 通信、WebSocket 频繁 | 高,专门为 Channels 设计 |
需要说明的是,现在很多项目即使不做 WebSocket,为了保持 Forced Async 的开放性,也推荐用 Uvicorn 或者带--worker-class uvicorn.workers.UvicornWorker的 Gunicorn。如果你要写自适应的 HTTP 和 WebSocket,我建议 Uvicorn,因为它本身就是一个生产级的 ASGI 服务器,配合--workers参数可以实现多进程,比单纯用 Gunicorn 配 worker class 更直观。
2.4 systemd 守护进程:让 Django 崩溃后自动拉起
最稳的启动方式是用 Linux 的 systemd 来管理 Django 进程。这样服务器重启之后服务可以自动启动,进程意外崩溃也能自动重启。我的做法是创建一个 systemd 服务文件,路径在/etc/systemd/system/myproject.service:
[Unit] Description=My Django Project After=network.target [Service] User=www-data Group=www-data WorkingDirectory=/var/www/myproject/backend ExecStart=/var/www/myproject/backend/venv/bin/gunicorn myproject.wsgi:application --bind 127.0.0.1:8000 --workers 3 Restart=always RestartSec=3 [Install] WantedBy=multi-user.target需要说明的是,User我特意设置为www-data。这样可以让 Nginx 和 Django 的进程以相同的用户身份运行,避免静态文件或 media 目录出现权限冲突。设置完后:
sudo systemctl daemon-reload sudo systemctl start myproject sudo systemctl enable myproject启动后一定要看日志确认有没有正常运行:
sudo journalctl -u myproject -f如果你的项目要部署在 80 端口之外的其他端口,比如 8080,那记得 Gunicorn 要监听127.0.0.1:8080,且 Nginx 里反向代理的目标地址也要改成这个端口。很多人这里不一致,导致 Nginx 反代过去发现连接不上。
2.5 数据库迁移与初始数据
这一步往往被人忽略。代码部署上去,但数据库表还没建,接口一调就报no such table。执行python manage.py migrate之前,确保数据库服务和账号权限是通的,如果是 MySQL,还要手动建好数据库并给用户授权;如果是 SQLite,确认db.sqlite3的文件目录有写权限,尤其是当你用www-data用户运行 Django 时,默认账户很可能没有写的权限。
创建超级管理员也很重要:
python manage.py createsuperuser有些项目会通过 fixture 导入初始数据,比如菜单配置、基础字典表,记得一并处理。我见过一个案例:代码全部正常,页面空白黑屏,排查了一整晚,最后发现是数据库里没有任何数据,Vue 页面首屏接口返回空数组,前端没做空状态处理,直接报错卡死了。
3. 前端 Vue 构建产物与 Nginx 的配合:dist 目录和 history 路由的底层逻辑
3.1 从 npm run build 到 dist 目录
Vue 项目的构建这一步本身很简单,但有两个前提条件。第一个是你要确认代码里面的接口地址是生产环境的地址,而不是http://localhost:8080这种本地地址。Vue 开发环境一般通过.env.development和.env.production两个文件区分环境变量:
// .env.production VITE_API_BASE_URL = "/api"我建议这样配置:前端所有请求都走相对路径/api,这样部署之后同源、没有跨域问题,也不需要在代码里硬编码 IP 地址。构建命令:
cd /var/www/myproject/frontend npm install npm run build构建完成后,产物会输出在dist/目录下。把dist/里的内容放到 Nginx 能访问的目录,比如/var/www/myproject/dist/,这一步可以直接用cp -r dist /var/www/myproject/。如果你用了 CI/CD(比如 Jenkins 或者 GitHub Actions),甚至可以省掉服务器上装 Node.js 这一步,直接在构建机上出产物,然后同步到服务器就行。
3.2 hash 模式和 history 模式:路由模式的部署差异
Vue Router 有两种模式:createWebHashHistory(hash 模式)和createWebHistory(history 模式)。默认脚手架生成的一般是createWebHistory,因为去掉#之后 URL 更干净,但部署环节对 Nginx 的要求不同。
hash 模式下,URL 会以/#/about这样的形式呈现,#之后的内容不会发送到服务器,Nginx 始终只收到domain.com/,所以不需要额外配置。而 history 模式,用户访问domain.com/about时,浏览器向服务器发送的请求就是/about,如果 Nginx 的配置只监听/并且没有做 fallback,就会返回 404。
生产环境里我一般推荐 history 模式,因为 URL 更美观、利于 SEO。代价是 Nginx 配置里必须在location /块内加一行try_files,把不存在路径的请求 fallback 到index.html:
location / { root /var/www/myproject/dist; index index.html; try_files $uri $uri/ /index.html; }这样,当用户访问/about时,Nginx 先去磁盘找dist/about文件,找不到就返回index.html,Vue Router 接管路由之后渲染对应的页面。这就是 history 模式能在刷新后不 404 的关键。
你还需要注意一个细节,前端打包后dist/下的index.html引用 JS、CSS 的路径默认是绝对路径/assets/xxx.js,如果你的站点部署在域名根路径下没问题;但如果部署在某个子路径(比如https://example.com/admin/),就要改 Vue 项目里的base配置,否则资源全部 404。一般我们部署整站就直接用域名根路径,省去这个麻烦。
3.3 前端和后端的接口联调:/api 的约定与跨域
前端页面和接口是同源部署的,即浏览器访问http://your-domain.com/,页面里的 JS 也请求http://your-domain.com/api/...,前后端同域名、同端口,所以不存在跨域问题。Nginx 在配置里要做的事很简单:把/api开头的路径全部转发给 Django。
如果前端直接请求http://your-domain.com:8000(Django 的端口),那就会出现跨域报错,需要在 Django 里配django-cors-headers。生产环境我建议还是老老实实用 Nginx 反代为同源。举个例子,用户在浏览器里点了某个按钮,Vue 的 axios 发出请求/api/user/info/,浏览器把这个请求发给 Nginx(80 端口),Nginx 一看路径是/api/,就把请求原样转发给127.0.0.1:8000(Django),Django 处理完返回 JSON,Nginx 再把这个响应回传给浏览器。整个过程中,浏览器只跟 Nginx 通信,没有任何跨域问题。
4. Nginx 站点配置逐行拆解:反向代理、静态文件与 WebSocket 支持
4.1 一份完整可用的 Nginx 站点配置
你需要修改 Nginx 的站点配置文件。Debian 系系统里,这个文件一般在/etc/nginx/sites-available/目录下,建议命名为myproject,然后做符号链接到sites-enabled:
sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/完整的配置如下,我把每一段的关键点拆开讲解:
server { listen 80; server_name your-domain.com; # 前端静态文件 location / { root /var/www/myproject/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 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; } # Django admin 静态文件 location /static/ { alias /var/www/myproject/backend/staticfiles/; } # 上传的媒体文件 location /media/ { alias /var/www/myproject/backend/media/; } }配置完成后测试语法并重新加载:
sudo nginx -t sudo systemctl reload nginx4.2 location 匹配规则与 alias/root 的区别
上面配置里有几个 location 块,很多人不理解 Nginx 是怎么匹配的,导致静态文件路径错乱。我先把 Nginx 的匹配规则讲清楚:精确匹配=优先级最高,然后是前缀匹配,Nginx 里最常用的^~表示优先前缀匹配,最后才是正则匹配。上面我们没用正则,用的是普通前缀匹配,/api/和/static/会分别命中自己的 location 块,而其他请求落到/,完璧归赵交给try_files。
关键点是 root 和 alias 的区别。root是把location匹配到的路径拼到 root 后面;alias是替换掉 location 后面的路径。举个例子:
location /static/ { root /var/www/myproject/backend/staticfiles/; }如果请求/static/css/app.css,Nginx 去找的路径是/var/www/myproject/backend/staticfiles/static/css/app.css,多了一层static,这就会 404。而alias的处理则不同:
location /static/ { alias /var/www/myproject/backend/staticfiles/; }请求/static/css/app.css,Nginx 去找的路径是/var/www/myproject/backend/staticfiles/css/app.css,正好对得上。所以动态文件最好用 proxy_pass(反代让后端处理),静态文件和上传文件用 alias,尤其不要混用 root 和 alias。
顺便一提,Django 的 admin 页面引用的 CSS 路径是/static/admin/css/base.css,假如你没有配location /static/,Nginx 就会把这个请求交给location /去查 dist 目录,结果显然 404。所以collectstatic+ Nginx 静态文件配置是两个必须做齐的步骤。
4.3 proxy_pass 结尾斜杠的陷阱
proxy_pass http://127.0.0.1:8000;后面到底要不要加斜杠,这是很多人踩过的坑。我直接告诉你结论:如果你希望浏览器请求/api/user/时,Django 收到的是/api/user/,那 proxy_pass 后面就不要加路径(只到端口为止),并且 location 后面要写/api/。如果 proxy_pass 后面加了路径,比如http://127.0.0.1:8000/,Nginx 会把 location 中匹配到的/api/前缀替换为空,Django 收到的就是/user/了。
实际效果取决于你在 Django 里的 URL 路由怎么写的。如果 Django 的接口路径本身就带了/api/(最常见的做法),那 proxy_pass 不要加斜杠;如果 Django 接口路径本身没有/api/,你想让 Nginx 剥掉前缀再转发,那就可以在 proxy_pass 后面加上http://127.0.0.1:8000并在末尾带一个/。我的建议是:让 Django 自己管理 URL 结构,Nginx 只做转发,不加斜杠,最不容易出错。
4.4 把转发头信息补齐:Django 拿不到真实 IP 的解决方案
生产环境里,Django 接收到的请求来源是127.0.0.1,因为 Nginx 在中间转发,真实用户的 IP 都在 Nginx 头上。如果不配置X-Forwarded-For,Django 里request.META.get('REMOTE_ADDR')拿到的永远是 Nginx 的地址,日志里全是 127.0.0.1,完全没法排查用户分布和做访问统计。
所以proxy_set_header X-Real-IP $remote_addr;和proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;这两行是必须的。如果项目里用了 Django 的django.middleware.common.CommonMiddleware,你还需要在settings.py里配置USE_X_FORWARDED_HOST = True,否则重定向的 host 会变成 Nginx 的地址而不是域名。
补充一个进阶配置:如果你后面还要做 HTTPS,在 Nginx 层终止 SSL 之后,转给 Django 的还是 HTTP,那么proxy_set_header X-Forwarded-Proto $scheme;能帮 Django 正确识别请求协议。Django 的settings.py里还需要配置:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")这个配置是为了防止 Django 把 HTTPS 请求当成 HTTP 处理,导致request.is_secure()判断错误,影响登录、支付回调等依赖安全性的功能。
4.5 如果项目用了 WebSocket,Nginx 需要额外配置
如果你的 Vue 项目里有实时消息、在线聊天、数据大屏推送之类的需求,后端用的是 Channels 或者原生的 async 视图,那你的 Nginx 反代不只是简单转发 HTTP,还得支持 WebSocket 的协议升级。Nginx 配置需要增加两个请求头:
location /ws/ { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }这里的关键是proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection "upgrade";,这两行告诉 Nginx:如果浏览器发来的是 WebSocket 握手请求,不要尝试以普通 HTTP 方式处理,请把连接升级为 WebSocket 协议,并建立一条基于 TCP 的长连接。没有这两行,你的 WebSocket 连接会在握手环节直接断开,前端new WebSocket()会进入onclose状态,报错信息也不直观。
需要说明的是,Django Channels 对外的服务端口和普通 Django 是分开的,通常我会让 Gunicorn 监听 8000 处理 HTTP,让 Daphne 监听 8001 处理 WS,这样互不干扰,也方便 Nginx 配置分流。
5. 上线后最容易踩的四个坑:从空白页到 502 的完整排查链路
5.1 API 大量 502 Bad Gateway:从 Nginx 日志定位
502 是部署后出现概率最高的错误码,原因也最直接:Nginx 连接不到它该反向代理的后端服务。大多数情况下,是 Gunicorn 没有启动或者启动之后崩了。排查过程我建议按固定顺序走,不要乱猜:
第一步,检查后端进程是否存在:
ps aux | grep gunicorn如果没有进程,说明服务没起来或者启动后崩了,直接看 systemd 日志:
sudo journalctl -u myproject -e --no-pager -n 50常见原因有几个:Gunicorn 报ModuleNotFoundError(说明 venv 没激活或者是用错了 Python 环境);PermissionError(说明www-data用户对项目目录没有读权限);数据库连接失败(migrate 没过或者数据库没起)。第二步,在服务器本机直接测试后端端口:
curl http://127.0.0.1:8000/api/ping/如果能返回 JSON,说明后端没毛病,问题出在 Nginx 的转发层。第三步,在 Nginx 配置里检查 proxy_pass 的地址和端口是否跟 Gunicorn 监听的一致。我犯过一次错:Gunicorn 监听的是 8001,Nginx 写的 8000,部署完怎么调都是 502,还固执地认为是防火墙问题,结果一看配置手一抖写歪了。
如果你的项目用了 Docker,排查思路类似:先看容器有没有起来,docker ps确认状态;再看端口映射有没有配;最后进容器里curl一下。Docker 场景下 502 还有一个常见原因是 Nginx 容器和后端容器不在同一个 Docker 网络里,Nginx 用服务名访问不到后端 IP,这时候把两个容器放到同一个 network 里就行了。
关于防火墙,多说一句:如果你的项目部署在云服务器上,云平台本身的安全组非常关键,但那种 502 通常表现为外网 curl 超时而不是立刻返回 502,和上面的情况要区分开。我建议先用curl http://你的域名/api/xxx/测试,如果返回 502 说明 Nginx 还活着,问题在后端;如果返回超时,多半是防火墙或者安全组的问题。
5.2 前端页面能打开,但接口请求 F12 全是 404
这种问题的表现是:页面内容显示出来了,Vue 框架也加载了,但接口报 404。原因通常是 Nginx 没有把/api/匹配到后端。有几种可能:
- Nginx 配置里忘了写
location /api/这个块,导致/api/...请求落到了location /,而location /里是try_files找静态文件,找不到就返回index.html,于是接口请求返回的不是 JSON 而是前端 HTML 页面。这种情况下 F12 里看到的响应其实是整个index.html的内容,前端 axios 解析 JSON 失败,报错信息往往是个对象转换异常。 - 写了
location /api但少了末尾斜杠。Nginx 的匹配是带前缀的,location /api会匹配/api和/apiabc,虽然大多时候也能用,但为了语义清晰还是写成location /api/。 - Django 的 URL 里本身没有
/api/前缀。比如你的接口路径是/api/v1/users/,但 Django 代码里的路由写的是/v1/users/,Nginx 转发过去之后 Django 匹配不到/api/v1/users/,自然 404。
排查 404 有一个通用思路:先看浏览器 F12 的完整请求 URL,再看 Nginx 的错误日志,最后看 Django 的访问日志。Nginx 的错误日志默认在/var/log/nginx/error.log,Django 的访问日志可以去journalctl里翻。请求链路上任何一个环节都能看到蛛丝马迹,不要只看一个地方。我甚至遇到过有人把 Nginx 的proxy_pass写成了http://127.0.0.1:8000/api/,导致请求被转成了api/api/v1/users/,Django 路由匹配失败返回 404,在 Nginx 层面看一切正常,但在后端日志里就能看到api/api/这种怪异路径。
5.3 刷新页面就 404 或者白屏,问题几乎都出在 history 模式
这个问题前面在讲路由模式时提到过,这里我再展开讲一下排查链路。现象:用户首次访问域名根路径能正常打开,但在某个子路由页面(比如/user/profile)按 F5 刷新,浏览器直接白屏或者 Nginx 返回 404。
根因就是:刷新时浏览器会向服务器发送一个/user/profile的 HTTP 请求,而 Nginx 在location /里如果没有try_files把不存在的路径 fallback 到index.html,就按照路径去磁盘上找文件,找不着就 404。
处理方案就是在location /里加try_files $uri $uri/ /index.html;。这一行可以说是 history 模式部署的灵魂。同时要注意,如果你在location /里配置了try_files,它的最后一个参数会作为内部重定向,Nginx 会按新的 URI 再执行一次请求处理。如果index.html本身也存在,并且和$uri都能正确匹配,就不会有问题。
有一种情况会让你白屏:你在 Nginx 里改完try_files之后忘了 reload,还在旧配置里看。我建议每次 Nginx 配置改动后,用nginx -t检查语法,再systemctl reload nginx,然后刷新浏览器(清缓存)验证。
5.4 CORS 跨域:本地开发好好的,上线就报跨域错
当你把前后端部署成同源之后,不具备跨域问题的前提(浏览器访问的源和接口的源是一样的),但现实是很多项目上线后依然会遇到跨域报错,Chrome 控制台报No 'Access-Control-Allow-Origin' header is present on the requested resource。
出现这种情况,九成是后端 Django 的django-cors-headers配置问题。我见过的最典型错误是:本地开发时为了省事,把CORS_ALLOW_ALL_ORIGINS = True,上线后忘了改。本地开发时前端localhost:8080访问后端localhost:8000,确实需要放开;但生产环境如果各管各的(比如前端部署在 A 服务器、后端在 B 服务器),你还得放开 B 服务器的跨域,而且允许的范围要精确控制:
CORS_ALLOWED_ORIGINS = [ "http://your-domain.com", "https://your-domain.com", ]还有一种情况:前后端是同源的(都通过 Nginx),但接口请求却带了跨域标识。比如前端页面访问http://your-domain.com:80,而后端反代目标写的是http://127.0.0.1:8000,Nginx 转发时如果没有正确设置Host,Django 重定向时可能生成一个带 8000 端口的 URL,浏览器收到后再去访问 8000 端口,于是跨域问题又冒出来了。解决这个问题的关键是proxy_set_header Host $host;。
从部署到排查的完整思路就是这样。我个人的建议是,不要相信“一次配置就能稳定运行一年”的侥幸心理,Nginx 和 Django 日志是排查问题的第一来源,哪里都排查不出来的时候,看日志永远有收获。上线后的第一周一定要勤翻日志,记录异常,后续你会少很多半夜被叫起来的请假时刻。
这个部署方案里,还有一个小细节我想单独提一下:如果你之后准备上 HTTPS,Nginx 配置里把 80 端口做成 301 跳转到 443,Django 侧再配好SECURE_PROXY_SSL_HEADER,整个链路才算完整。我会建议你把证书签发和自动续期也提前规划好,省得每年手动折腾一次。作为整个部署方案的延伸,用脚本把collectstatic、migrate、systemctl restart串起来做成一键部署,是省下重复劳动成本的最直接方式。