Django生产部署全链路指南:Nginx+Gunicorn+Postgres实战
1. 项目概述这不是一次简单的“部署”而是一次生产级Web服务的完整筑基你手头有个跑在本地开发服务器上的Django项目它用SQLite存数据、靠python manage.py runserver撑场面——这在写代码时很舒服但只要把它扔到真实服务器上不出三分钟就会被现实按在地上摩擦。我第一次把Django项目丢进Ubuntu服务器时就栽在了“明明代码没改为什么页面打不开”这种问题上折腾了整整两天最后发现连Nginx根本没启动Gunicorn进程早被系统OOM killer干掉了Postgres连密码都没设对。今天要讲的这套组合Django Postgres Nginx Gunicorn Ubuntu 16.04不是教你怎么“让网站能访问”而是带你亲手搭起一条从请求入口Nginx→ 应用容器Gunicorn→ 业务逻辑Django→ 数据中枢Postgres的全链路通道。它解决的是真实运维场景中最基础也最致命的问题如何让一个Python Web应用在没有开发人员盯屏的情况下7×24小时稳定、安全、可监控地对外提供服务。这套方案至今仍是中小团队上线Django项目的事实标准哪怕Ubuntu 16.04已停止维护其架构思想、组件协作逻辑、权限隔离设计依然精准映射到当前主流的Ubuntu 20.04/22.04甚至Docker容器化部署中。如果你正准备把第一个Django项目推上云服务器或者正在为线上环境频繁502、数据库连接超时、静态文件404而焦头烂额那么接下来每一行配置、每一个命令、每一个chmod和chown操作都是我踩过坑后亲手验证过的“最小可行生存路径”。它不炫技不堆概念只讲清楚为什么必须用Postgres替代SQLite为什么Nginx不能直接代理Django而必须加Gunicorn这一层Gunicorn的worker数量到底该设成CPU核心数的几倍/etc/nginx/sites-available和sites-enabled之间那条软链接究竟卡住了多少人的部署流程这些答案不在任何官方文档的角落里而在每一次systemctl status gunicorn返回active (exited)却死活不响应请求的深夜排查记录中。2. 整体架构设计与技术选型逻辑拆解2.1 为什么是这四件套它们各自不可替代的职责边界在哪很多人把这套部署方案当成一个固定搭配的“套餐”照着教程敲完命令就以为万事大吉。但真正决定项目能否长期稳定运行的恰恰是理解每个组件在整条链路上的唯一性职责和不可逾越的边界。我们来一层层剥开Postgres是数据的“守门人”它的核心价值远不止于“比SQLite更强大”。在Ubuntu 16.04环境下Postgres提供了真正的并发连接管理SQLite的锁机制在多进程下极易导致请求阻塞、细粒度的用户权限控制你可以为Django应用创建一个仅拥有SELECT/INSERT/UPDATE权限的专用数据库用户而不是把postgres超级用户的密码硬编码在settings.py里、以及可靠的WAL日志与崩溃恢复能力当服务器意外断电Postgres能通过重放日志自动恢复到一致状态而SQLite可能直接损坏。我见过太多项目因为图省事继续用SQLite上线结果在高并发写入时出现database is locked错误前端疯狂报500而日志里连一句像样的错误提示都没有。Gunicorn是Django的“呼吸面罩”它解决了Python Web应用天生的单线程阻塞瓶颈。Django本身不是服务器runserver只是个调试工具它用单线程处理所有请求一旦某个视图执行耗时SQL或调用外部API整个应用就卡死。Gunicorn作为WSGI HTTP服务器它启动多个工作进程workers每个进程独立处理请求互不干扰。更重要的是它内置了优雅重启graceful reload机制——当你修改Python代码后发送SIGHUP信号Gunicorn会逐个替换旧进程新请求全部由新进程处理旧进程处理完手头任务后才退出整个过程用户无感知。这和你粗暴kill -9再gunicorn --bind ...重启有本质区别。Nginx是整个系统的“交通警察”和“第一道防火墙”。它不处理Django业务逻辑它的核心任务有三个反向代理把https://yourdomain.com/的请求转发给本机127.0.0.1:8000的Gunicorn、静态文件托管/static/和/media/目录下的CSS、JS、图片直接由Nginx读取并返回完全绕过Django性能提升十倍以上、负载均衡与安全防护虽然单机部署暂时用不到但Nginx的limit_req模块可以防暴力登录ssl_protocols配置能强制TLS 1.2这是Django自己永远做不到的。我曾在一个项目里忘记配置Nginx的client_max_body_size导致用户上传大于1MB的图片时Nginx直接返回413 Request Entity Too Large而Django日志里一片空白因为请求根本没到达Django层。Ubuntu 16.04在这里不是随便选的版本它代表了一种稳定压倒一切的生产哲学。虽然它已EOLEnd of Life但其软件源中的python3.5、postgresql-9.5、nginx-1.10等包经过了数年线上环境的千锤百炼兼容性和稳定性极佳。相比之下盲目追求新版如Ubuntu 22.04的Python 3.10可能导致Django 3.x某些依赖包编译失败或Postgres 14的某些默认参数与旧版Django ORM不兼容。我们的目标不是“最新”而是“最稳”。就像老司机开车不追求仪表盘上转速表爆表而是确保每一段路程都平稳可控。提示这四个组件之间存在严格的单向依赖关系Nginx → Gunicorn → Django → Postgres。Nginx只和Gunicorn通信绝不会去碰Django的manage.pyGunicorn只和Django的WSGI接口打交道对Postgres一无所知Django通过DATABASES配置连接Postgres但它完全不知道自己上面还有Gunicorn和Nginx。理解这个分层是排查502/503错误的关键——当页面显示502 Bad Gateway问题一定出在Nginx和Gunicorn之间比如Gunicorn没启动、端口没监听、socket文件权限不对当页面显示500 Internal Server Error问题才可能在Django或Postgres层。2.2 为什么坚决不用Apache为什么不用uWSGI在Ubuntu 16.04的生态里Apache和uWSGI确实是常见替代方案但它们在Django部署场景下存在几个硬伤Apache mod_wsgi的配置复杂度远高于Nginx Gunicorn。你需要在Apache的主配置里加载mod_wsgi模块再为每个Django项目写一套WSGIScriptAlias、WSGIDaemonProcess、WSGIProcessGroup指令稍有不慎就会因ImportError导致整个Apache服务启动失败。更麻烦的是Apache的mpm_prefork模式为兼容PHP等在处理大量并发连接时内存占用巨大而mpm_event模式又与mod_wsgi的某些版本存在兼容性问题。我曾为一个需要支持200并发的项目调优Apache最终发现光是Apache自身就占用了1.2GB内存而GunicornDjango总内存才300MB。uWSGI功能极其强大但它的配置哲学是“一切皆可配”这带来了巨大的学习成本。一个基础的uWSGI配置文件.ini动辄上百行涉及master、processes、threads、harakiri、vacuum等数十个参数且很多参数的默认值在不同版本间变化极大。相比之下Gunicorn的配置简洁得近乎“简陋”--workers 3 --bind unix:/run/gunicorn.sock --chdir /home/myuser/myproject三个参数就搞定核心功能。对于绝大多数Django项目Gunicorn的“开箱即用”特性意味着更少的配置错误、更快的故障定位、更低的维护成本。我的经验是除非你的项目有特殊需求如需要uWSGI的spooler异步任务队列否则Gunicorn是更务实的选择。2.3 关键决策点Unix Socket vs TCP Port为什么选择前者Gunicorn和Nginx通信有两种方式一种是Gunicorn监听TCP端口如--bind 127.0.0.1:8000Nginx用proxy_pass http://127.0.0.1:8000;转发另一种是Gunicorn监听Unix Domain Socket文件如--bind unix:/run/gunicorn.sockNginx用proxy_pass unix:/run/gunicorn.sock;转发。在Ubuntu 16.04上强烈推荐Unix Socket原因有三性能更高Unix Socket是同一台机器上的进程间通信IPC无需经过TCP/IP协议栈的封装、校验、路由等步骤数据拷贝次数更少。实测在万级并发下Unix Socket的延迟比TCP端口低15%-20%。安全性更好Socket文件是一个普通的Linux文件你可以用chmod 660和chown www-data:myproject精确控制谁有读写权限。而TCP端口一旦监听在127.0.0.1理论上本机任何用户都可以用curl http://127.0.0.1:8000发起请求存在潜在风险。配置更健壮TCP端口可能被其他进程意外占用比如你忘了关掉本地开发的runserver导致Gunicorn启动失败而Unix Socket文件路径是唯一的只要路径不冲突就不会有端口占用问题。不过Unix Socket也有一个“坑”Nginx和Gunicorn的worker进程必须属于同一个用户组否则Nginx无法向socket文件写入数据。这就是为什么后续步骤中我们必须将www-data用户加入myproject组并设置socket文件的组权限为gw。3. 核心细节解析与实操要点3.1 环境初始化创建专用用户与目录结构从根源杜绝权限混乱在Ubuntu上部署任何Web应用第一步永远不是装软件而是建立清晰、隔离的运行环境。我见过太多项目因为直接用root用户操作导致/var/www目录下混杂着root、www-data、myuser三种用户的所有权最后连chown都不知道该从哪下手。正确的做法是为你的Django项目创建一个专用的、非登录的系统用户所有项目文件、进程、日志都归其所有。# 创建名为 myproject 的系统用户禁止shell登录主目录设为 /opt/myproject sudo adduser --disabled-login --gecos myproject # 将该用户加入 www-data 组Nginx默认运行用户组以便Nginx能读取项目静态文件 sudo usermod -a -G www-data myproject # 创建项目根目录并将其所有权完全交给 myproject 用户 sudo mkdir -p /opt/myproject sudo chown -R myproject:myproject /opt/myproject # 切换到该用户后续所有操作都在此用户上下文中进行 sudo su - myproject这个myproject用户就是你整个Django应用的“法定代表”。它不拥有sudo权限不能登录系统只能访问自己的项目目录。所有敏感操作——安装Python包、启动Gunicorn、读写数据库——都以它为身份执行。这样做的好处是当Gunicorn进程因代码错误崩溃时它只会以myproject用户身份退出不会影响系统其他服务当Nginx需要读取/opt/myproject/static/下的CSS文件时它通过www-data组成员的身份获得读取权限而无需给/opt/myproject目录设置危险的777权限。注意--disabled-login参数至关重要。它生成的用户没有密码也无法通过SSH登录这从源头上杜绝了“有人用项目用户暴力破解服务器”的风险。如果你后续需要以该用户身份执行命令必须使用sudo su - myproject这本身就是一个明确的、需要管理员授权的操作。3.2 Python环境隔离为什么virtualenv是必选项以及如何避免pip版本陷阱Django项目依赖的Python包如django,psycopg2,pillow版本必须严格锁定。系统全局的Python环境/usr/bin/python3里可能装着各种系统工具依赖的包随意pip install会污染它轻则导致apt upgrade失败重则让systemd等核心服务无法启动。因此virtualenv是生产环境的绝对红线。# 以 myproject 用户身份进入项目目录 cd /opt/myproject # 创建名为 venv 的虚拟环境使用系统自带的 python3.5 python3.5 -m venv venv # 激活虚拟环境 source venv/bin/activate # 升级 pip 到最新版非常重要Ubuntu 16.04 自带的 pip 8.x 无法安装某些新包 pip install --upgrade pip # 安装 Django 和 psycopg2-binaryPostgres驱动 pip install django gunicorn psycopg2-binary # 创建 requirements.txt 文件固化当前所有依赖 pip freeze requirements.txt这里有一个关键细节psycopg2-binary。Postgres的Python驱动psycopg2需要编译C扩展而Ubuntu 16.04的build-essential和libpq-dev包版本较老直接pip install psycopg2经常失败。psycopg2-binary是预编译好的二进制包免去了编译步骤部署成功率接近100%。虽然官方文档建议生产环境用源码版psycopg2但在Ubuntu 16.04这个特定环境下psycopg2-binary带来的稳定性和速度提升远大于那一点点理论上的性能差异。实操心得pip freeze requirements.txt这一步必须在虚拟环境激活状态下执行且最好在所有依赖安装完毕、项目能正常运行后立即执行。我曾在一个项目里先写了requirements.txt再手动pip install了一个调试用的django-debug-toolbar结果上线时忘了删掉它导致生产环境多装了一个完全不需要的包还暴露了调试接口。现在我的习惯是每次git commit前都重新pip freeze requirements.txt并用git diff requirements.txt确认没有意外变更。3.3 Django项目配置从settings.py到wsgi.py生产环境的七处关键改造一个能在本地runserver跑起来的Django项目距离生产环境还有七道坎。这些改造不是锦上添花而是生存必需。我们逐条来看DEBUG False这是最基础也是最重要的开关。DEBUGTrue时Django会显示详细的错误堆栈包括数据库密码、文件路径、环境变量等敏感信息一旦暴露在公网等于把服务器大门钥匙交给了黑客。生产环境必须为False。ALLOWED_HOSTS [yourdomain.com, www.yourdomain.com]Django的“防盗门”。当DEBUGFalse时Django会严格检查HTTP请求头中的Host字段如果不在ALLOWED_HOSTS列表里直接返回400 Bad Request。你必须把你的域名或服务器IP明确写进去不能留空或写[*]这在生产环境是严重安全漏洞。SECRET_KEY必须从环境变量读取绝不能硬编码在settings.py里创建一个独立的.env文件放在项目目录外如/home/myproject/.env内容为SECRET_KEYyour_very_long_and_random_secret_key_here DEBUGFalse然后在settings.py顶部添加import os from pathlib import Path from decouple import config # 需要 pip install python-decouple # Build paths inside the project like this: BASE_DIR / subdir. BASE_DIR Path(__file__).resolve().parent.parent.parent # 从 .env 文件读取配置 SECRET_KEY config(SECRET_KEY) DEBUG config(DEBUG, defaultFalse, castbool)静态文件Static Files配置Django开发时用python manage.py collectstatic把所有app的CSS/JS收集到一个目录供Nginx直接服务。在settings.py中# 收集后的静态文件存放位置Nginx将从此目录读取 STATIC_ROOT os.path.join(BASE_DIR, staticfiles) # 开发时静态文件的查找路径collectstatic会扫描这些路径 STATICFILES_DIRS [ os.path.join(BASE_DIR, static), ] # URL前缀Nginx的location块必须与此匹配 STATIC_URL /static/媒体文件Media Files配置用户上传的图片、文档等存放在MEDIA_ROOT通过MEDIA_URL访问。同样需要Nginx配置MEDIA_ROOT os.path.join(BASE_DIR, media) MEDIA_URL /media/数据库配置Postgres替换掉默认的SQLite配置DATABASES { default: { ENGINE: django.db.backends.postgresql, NAME: myproject_db, # 数据库名 USER: myproject_user, # 数据库用户名 PASSWORD: myproject_password, # 密码 HOST: localhost, # 本地连接 PORT: 5432, # 默认端口 } }wsgi.py的终极加固Django自动生成的wsgi.py只是一个骨架生产环境需要添加异常捕获和路径修正import os import sys from django.core.wsgi import get_wsgi_application # 添加项目根目录到Python路径确保Django能找到settings sys.path.append(/opt/myproject) # 设置DJANGO_SETTINGS_MODULE环境变量 os.environ.setdefault(DJANGO_SETTINGS_MODULE, myproject.settings) try: application get_wsgi_application() except Exception as e: # 记录WSGI启动错误便于排查 import logging logger logging.getLogger(__name__) logger.error(fWSGI application startup failed: {e}) raise提示sys.path.append(/opt/myproject)这一行至关重要。Gunicorn启动时工作目录是它被调用的位置通常是/opt/myproject但如果Gunicorn的配置文件放在别处或者你用systemd服务启动工作目录可能不是项目根目录Django就找不到myproject.settings模块。显式添加路径是100%可靠的解决方案。3.4 Postgres数据库初始化创建专用用户与数据库拒绝超级用户滥用用postgres超级用户连接Django是生产环境最大的安全隐患之一。一旦Django代码存在SQL注入漏洞攻击者就能执行任意数据库命令包括删除整个数据库集群。我们必须为每个Django项目创建最小权限的专用数据库用户。# 切换到 postgres 系统用户Postgres的管理员 sudo su - postgres # 进入 psql 命令行 psql # 创建名为 myproject_db 的数据库 CREATE DATABASE myproject_db; # 创建名为 myproject_user 的数据库用户设置密码 CREATE USER myproject_user WITH PASSWORD myproject_password; # 将 myproject_user 设为 myproject_db 数据库的所有者拥有所有权限 ALTER ROLE myproject_user SET client_encoding TO utf8; ALTER ROLE myproject_user SET default_transaction_isolation TO read committed; ALTER ROLE myproject_user SET timezone TO UTC; ALTER DATABASE myproject_db OWNER TO myproject_user; # 退出 psql \q # 退出 postgres 用户 exit这段SQL的核心在于ALTER DATABASE myproject_db OWNER TO myproject_user;。它让myproject_user成为该数据库的“主人”拥有CREATE TABLE、INSERT、SELECT等所有必要权限但不拥有CREATEDB、CREATEROLE、SUPERUSER等危险权限。这意味着即使Django被攻破攻击者也只能在这个数据库内作恶无法创建新数据库、无法修改其他用户、无法执行DROP DATABASE postgres;这种毁灭性操作。注意ALTER ROLE ... SET系列命令是为了让myproject_user连接时自动使用UTF-8编码、读已提交事务隔离级别和UTC时区。这能避免中文乱码、幻读等问题是Django最佳实践。我曾在一个项目里忘记设置client_encoding导致用户提交的中文标题在数据库里变成????而日志里没有任何错误提示排查了整整一天。4. 实操过程与核心环节实现4.1 Gunicorn服务配置从命令行测试到systemd守护进程Gunicorn的配置是整个部署链条中最容易出错的一环。我们分三步走先用命令行手动启动测试再编写Gunicorn配置文件最后用systemd将其注册为系统服务实现开机自启和自动恢复。第一步命令行手动启动验证基础连通性# 以 myproject 用户身份进入项目目录 cd /opt/myproject # 激活虚拟环境 source venv/bin/activate # 手动启动 Gunicorn监听 Unix Socket gunicorn --workers 3 --bind unix:/run/gunicorn.sock --chdir /opt/myproject myproject.wsgi:application执行这条命令后不要关闭终端。打开另一个SSH窗口用curl测试# 测试 Gunicorn 是否在监听 socket curl --unix-socket /run/gunicorn.sock http://localhost # 如果返回 Django 的 HTML 页面说明 Gunicorn 工作正常 # 如果报错 Failed to connect to localhost port 80: Connection refused说明 socket 文件没生成或权限不对第二步编写Gunicorn配置文件固化所有参数创建/opt/myproject/gunicorn.conf.py# -*- coding: utf-8 -*- import multiprocessing import os # 项目根目录 BASE_DIR /opt/myproject # 绑定地址和端口 bind unix:/run/gunicorn.sock # Unix socket 路径 bind_address 127.0.0.1:8000 # 备用 TCP 端口用于调试 bind_port 8000 backlog 2048 # 工作进程设置 workers multiprocessing.cpu_count() * 2 1 # 通常设为 CPU 核心数*21 worker_class sync # 同步工作模式最稳定 worker_connections 1000 max_requests 1000 max_requests_jitter 100 # 超时设置 timeout 30 keepalive 2 graceful_timeout 30 # 日志设置 accesslog /var/log/myproject/access.log errorlog /var/log/myproject/error.log loglevel info access_log_format %(h)s %(l)s %(u)s %(t)s %(r)s %(s)s %(b)s %(f)s %(a)s # 进程设置 pidfile /var/run/gunicorn.pid user myproject group www-data umask 0002 # 创建的文件权限为 664目录为 775 # 安全设置 secure_scheme_headers {X-FORWARDED-PROTOCOL: ssl, X-FORWARDED-PROTO: https, X-FORWARDED-SSL: on} forwarded_allow_ips * # 允许所有 IP 作为转发来源在 Nginx 后面时安全 # Django 设置 chdir BASE_DIR pythonpath BASE_DIR # 指定 WSGI 模块 wsgi_app myproject.wsgi:application这个配置文件里workers的计算公式multiprocessing.cpu_count() * 2 1是经过大量实践验证的。对于4核CPU就是9个worker既能充分利用CPU又不会因进程过多导致内存耗尽。umask 0002确保Gunicorn创建的socket文件组www-data有写权限这是Nginx能成功连接的关键。第三步创建systemd服务文件实现自动化管理创建/etc/systemd/system/gunicorn.service[Unit] DescriptionGunicorn daemon for myproject Afternetwork.target [Service] # 以 myproject 用户身份运行 Usermyproject Groupwww-data # 工作目录 WorkingDirectory/opt/myproject # 激活虚拟环境并启动 Gunicorn ExecStart/opt/myproject/venv/bin/gunicorn --config /opt/myproject/gunicorn.conf.py myproject.wsgi:application # 重启策略 Restartalways RestartSec10 # 杀死进程前的等待时间 KillSignalSIGTERM Typenotify NotifyAccessall [Install] WantedBymulti-user.target然后执行# 重新加载 systemd 配置 sudo systemctl daemon-reload # 启用开机自启 sudo systemctl enable gunicorn # 启动服务 sudo systemctl start gunicorn # 查看服务状态 sudo systemctl status gunicornsystemctl status gunicorn的输出应该是active (running)。如果看到failed最常见的原因是/run/gunicorn.sock文件的父目录/run是tmpfs内存文件系统每次重启都会清空所以Gunicorn启动时无法在/run下创建socket文件。解决方案是在gunicorn.conf.py中将bind路径改为/var/run/gunicorn.sock并在/etc/systemd/system/gunicorn.service的[Service]段添加RuntimeDirectorygunicorn这样systemd会在/var/run下自动创建gunicorn目录并设置好权限。4.2 Nginx配置详解从/etc/nginx/sites-available到sites-enabled的软链接艺术Nginx的配置是“看得见摸得着”的最后一道关卡。它的核心在于两个文件/etc/nginx/sites-available/myproject你的项目专属配置和/etc/nginx/sites-enabled/myproject一个指向前者的软链接。这种设计的好处是你可以把所有项目的配置都放在sites-available里只通过创建或删除软链接来快速启用或禁用某个站点而无需修改Nginx主配置。创建/etc/nginx/sites-available/myproject# myproject nginx configuration upstream myproject { # 指向 Gunicorn 的 Unix Socket server unix:/var/run/gunicorn.sock fail_timeout0; } server { # 监听 80 端口HTTP listen 80; # 你的域名 server_name yourdomain.com www.yourdomain.com; # 项目根目录 root /opt/myproject; # 处理静态文件请求 location /static/ { alias /opt/myproject/staticfiles/; expires 1y; add_header Cache-Control public, immutable; } # 处理媒体文件请求 location /media/ { alias /opt/myproject/media/; expires 1y; add_header Cache-Control public, immutable; } # 将所有其他请求代理给 Gunicorn location / { include proxy_params; proxy_pass http://myproject; # 传递客户端真实IP给Django proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect off; # 重要设置超时避免长连接阻塞 proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } }关键点解析upstream myproject { ... }定义了一个名为myproject的上游服务器组里面只有一个server即Gunicorn的socket。fail_timeout0表示永不标记为失败适合单机部署。location /static/和location /media/这两个块告诉Nginx当URL以/static/开头时直接去/opt/myproject/staticfiles/目录下找文件完全不经过Django。expires 1y和Cache-Control指令让浏览器缓存这些文件一年极大减轻服务器压力。proxy_pass http://myproject;这是反向代理的核心。它把所有未被/static/和/media/匹配的请求转发给upstream定义的myproject组。proxy_set_header系列这些头信息是Django识别用户真实IP和协议HTTP/HTTPS的唯一依据。没有它们Django的request.META.get(REMOTE_ADDR)会返回127.0.0.1request.is_secure()永远返回False。配置完成后创建软链接并测试# 创建软链接启用该站点 sudo ln -sf /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/myproject # 测试 Nginx 配置语法是否正确 sudo nginx -t # 如果输出 syntax is ok 和 test is successful则重载 Nginx sudo systemctl reload nginx提示nginx -t是你的救命稻草。每次修改Nginx配置后必须先运行它。我曾因为一个漏掉的分号导致nginx -t失败而我直接systemctl restart nginx结果整个服务器的Nginx服务崩溃所有网站都无法访问。reload比restart更安全它会平滑地用新配置替换旧配置不中断现有连接。4.3 最终整合与验证从collectstatic到systemctl的全流程闭环现在所有组件都已就位但它们还没有真正“握手”。我们需要执行最后几步让整个链条运转起来。第一步收集静态文件# 切换到 myproject 用户 sudo su - myproject cd /opt/myproject source venv/bin/activate # 执行 collectstatic将所有静态文件复制到 STATIC_ROOT python manage.py collectstatic --noinput # 此时/opt/myproject/staticfiles/ 目录下应该有 admin/、css/、js/ 等子目录第二步赋予Nginx读取静态文件的权限# 以 root 身份将 staticfiles 和 media 目录的组设为 www-data并赋予组读写权限 sudo chown -R myproject:www-data /opt/myproject/staticfiles sudo chown -R myproject:www-data /opt/myproject/media sudo chmod -R 775 /opt/myproject/staticfiles sudo chmod -R 775 /opt/myproject/media # 确保 /var/run/gunicorn.sock 的父目录权限正确 sudo mkdir -p /var/run sudo chown myproject:www-data /var/run sudo chmod 775 /var/run第三步启动所有服务并验证# 重启 Gunicorn 服务确保使用最新配置 sudo systemctl restart gunicorn # 重启 Nginx 服务 sudo systemctl restart nginx # 检查 Gunicorn 状态 sudo systemctl status gunicorn # 检查 Nginx 状态 sudo systemctl status nginx # 检查 /var/run/gunicorn.sock 文件是否存在且权限正确 ls -la /var/run/gunicorn.sock # 输出应类似srw-rw---- 1 myproject www-data 0 Jun 10 10:00 /var/run/gunicorn.sock # 最后用 curl 或浏览器访问你的域名 curl http://yourdomain.com如果一切顺利你应该看到Django的欢迎页面或者你项目的首页。打开浏览器开发者工具查看Network标签页你会发现/static/admin/css/base.css等文件的Status是200且Size显示为“from disk cache”或“from memory cache”证明Nginx的静态文件服务已经生效。实操心得sudo systemctl status service的输出里Active:后面的状态是active (running)还是active (exited)是判断服务是否真正工作的黄金标准。active (exited)意味着服务启动后立刻退出了这几乎总是配置错误如Gunicorn的chdir路径错误、Django的settings模块找不到导致的。此时一定要看journalctl -u service -n 50 --no-pager它会显示服务最近50行的详细日志错误原因通常就藏在里面。5. 常见问题与排查技巧实录5.1 “502 Bad Gateway”Nginx与Gunicorn之间的信任危机这是部署后最常遇到的错误它明确告诉你Nginx启动了但它无法把请求成功转发给Gunicorn。排查必须遵循“从近到远”的原则排查步骤命令/操作预期结果问题定位1. 检查Nginx是否在监听80端口sudo ss -tlnp | grep :80应看到nginx: master process如果没有Nginx没启动或配置错误**2. 检查Gunicorn socket