在宝塔上部署 Django 项目
这篇讲什么
- 这篇原来叫《在宝塔下使用 uwsgi 上线 Django 项目》,现在把范围放大:宝塔面板 + uWSGI 的完整流程仍然保留(它是很多人唯一会用的面板),但把默认推荐换成 Gunicorn + systemd + Nginx——uWSGI 已经不是官方推荐路线,配置和排错都更费劲。
- 面板部分只校对对错、不做实机复现(那是你自己的服务器,我不去动);Gunicorn/systemd/Nginx 这三步全部在 WSL2 上真跑过,实测输出贴在下面。
- 实测环境:WSL2 Ubuntu 26.04.1 LTS(systemd 已启用)+ Python 3.14.4 + Django 5.2.19 LTS + Gunicorn 26.2.0 + Uvicorn 0.54.0 + Nginx 1.28.3 + MySQL 9.7.4。
- Docker / docker-compose 部分按官方文档写可复制步骤,本机没有容器运行时,未验证。
四种部署形态怎么选
| 形态 | 谁管进程 | 谁服务静态 | 上手难度 | 适合 |
|---|---|---|---|---|
| 宝塔面板 + uWSGI | 面板/start.sh |
Nginx(面板配) | 最低,全程点鼠标 | 只有一台 VPS、不想碰命令行 |
| Gunicorn + systemd + Nginx | systemd | Nginx | 中(三个配置文件) | 默认推荐,官方文档也点名 Gunicorn |
| Uvicorn + systemd + Nginx | systemd | Nginx | 中 | 用到异步视图 / WebSocket |
| Docker Compose | 容器 + restart 策略 | Nginx 容器 | 中高(要学镜像与卷) | 多环境一致、CI/CD 交付 |
一句话原则:能用 systemd 就别用 start.sh + daemonize,进程守护、开机自启、日志收集、崩溃重启这四件事 systemd 都做得更可靠。
宝塔面板 + uWSGI 上线
部署准备
- 购买服务器和备案域名(这里以 CentOS 8.x 为例)。
- 在服务器上安装宝塔面板以及 Nginx、MySQL、Python 项目管理等组件。
- 准备要上线的 Django 项目,先在本地确认
python manage.py check无错、依赖能装齐。
添加站点
-
添加站点

-
上传项目


部署项目
-
安装你需要的 Python 和 pip 版本
宝塔的「Python 项目管理」里可以直接装指定版本的 Python;命令行方式用系统的包管理器或 pyenv,官方口径见 Python 官方安装文档 与发行版文档(CentOS 用
dnf,Ubuntu 用apt)。 -
修改
settings.pysettings.py 1
2
3
4
5# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
# 老笔记这里写的是 ['*'],生产上不推荐:等于放弃 Host 头校验
ALLOWED_HOSTS = ["你的域名", "www.你的域名", "服务器公网IP"]['*']是开发偷懒写法。DEBUG=False时 Django 会校验Host头,不在名单里直接 400(实测见《Django-生产环境要注意哪些事项》1.2),写死域名才是正解。 -
放行相应端口
-
放行你需要的端口:Nginx 的 80/443、SSH 端口。不要为了测试把 Django 的 8000、MySQL 的 3306、Redis 的 6379 对公网开放。老笔记里写的「Redis 的 27017 端口」是笔误,27017 是 MongoDB,Redis 默认端口是 6379。
-
面板放行之后,还要去云厂商控制台的安全组放行同样的端口,两边都通才算通。

-
临时验证可以在项目目录跑
python manage.py runserver 0.0.0.0:8000(老笔记写的是0:8000,效果相同但可读性差),本地用ip:8000访问;不通就回去查安全组。验证完必须 Ctrl+C 关掉,runserver不是生产服务器。
-
-
添加
uwsgi.ini配置文件官方文档:Django 如何部署到 uWSGI。在项目目录新建
uwsgi.ini:1
2
3
4
5
6
7
8
9
10
11[uwsgi]
; 用 uwsgi 协议对接 Nginx,只监听回环,别写 0.0.0.0
socket=127.0.0.1:8000
; http=127.0.0.1:8000 ; 只有不用 Nginx 直连时才用 http
chdir=/www/wwwroot/你的域名/你的项目/
wsgi-file=shop/wsgi.py
processes=4
threads=2
master=true
pidfile=uwsgi.pid
daemonize=uwsgi.log参数说明:
socket:Nginx 用uwsgi_pass对接时用的协议端口;http是让 uWSGI 直接当 HTTP 服务器。chdir:项目目录(manage.py所在层)。wsgi-file:wsgi.py相对chdir的路径。processes/threads:进程数与每进程线程数。master:是否启用管理进程,写true(老笔记写True,大小写不属于文档约定写法)。pidfile:主进程 pid 的存放文件,--stop要用它。daemonize:守护进程化并把日志写到该文件。- 老笔记把
socket写成0.0.0.0:8000,等于把 uWSGI 协议端口挂到公网,任何人都能直接打后端、绕过 Nginx 的限流与 TLS,务必改成127.0.0.1或 unix socket。
-
添加 uWSGI 控制命令
1
2
3
4
5# start.sh
uwsgi -d --ini uwsgi.ini
# stop.sh
uwsgi --stop uwsgi.pid权限用
chmod 750 start.sh stop.sh就够,不要用chmod 777(老笔记里是 777),否则任何本机账号都能改脚本内容。 -
添加依赖文件
- 项目目录下新建
requirements.txt(老笔记两处拼成了requiremens.txt),列出运行依赖。 - 本地用
pip freeze > requirements.txt生成,但生成后要手工删掉与部署无关的包,并按需锁版本。
- 项目目录下新建
-
迁移静态文件
settings.py 1
2
3STATIC_URL = "/static/"
STATIC_ROOT = "/www/wwwroot/你的域名/static/"
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]STATIC_ROOT不能出现在STATICFILES_DIRS里,否则collectstatic会直接报错拒绝执行(官方约束)。1
$ python manage.py collectstatic --noinput
不必再单独写
collectstatic.sh并chmod 777,把它做成面板的计划任务或发布脚本的一步即可。 -
配置 Nginx
-
进入站点配置界面

-
配置 Nginx 图示

-
在最上边添加负载均衡模块
1
2
3
4
5upstream project {
server 10.11.0.1:8000 down;
server 10.11.0.2:8000 backup;
server 127.0.0.1:8000 weight=1 max_fails=1 fail_timeout=300s;
}project:负载均衡组的名字,后面uwsgi_pass project;直接引用。- 调度算法:
weight(加权轮询)、ip_hash(会话保持)、least_conn(最少连接,Nginx 自带)、fair/url_hash(第三方模块提供)。各算法的实测分流结果见《Django-应用水平扩展》第四节。 down:该节点暂时不参与分发。backup:其它节点都不可用时才启用。max_fails/fail_timeout:连续失败多少次、暂停多久,属于被动健康检查。- 注意:算法为
ip_hash时,节点状态不能配weight和backup。 - 说明:
worker_rlimit_nofile触顶时新连接会报 502;log_format定义好格式后要用access_log指定落盘路径(这两条来自官方文档,未在本机复现)。
-
配置 location
1
2
3
4
5location / {
include uwsgi_params;
uwsgi_pass project;
uwsgi_param SERVER_NAME $server_name;
}用
socket(uWSGI 协议)就必须是uwsgi_pass;如果 uWSGI 侧开的是http,这里要改成proxy_pass,两者混用会 502。 -
配置静态目录
1
2
3location /static {
alias /www/wwwroot/你的域名/static/;
} -
注释宝塔 Nginx 配置

注意:修改完记得保存,并先
nginx -t再重载。 -
重载 Nginx 配置

-
-
添加 SSL
面板「SSL」页可以一键申请 Let’s Encrypt 证书并强制 HTTPS(面板会写跳转规则)。证书相关原理与
certbot命令行方式见《Django-生产环境中的安全》第三节。
-
启动项目
1
2
3
4
5$ pip install -r requirements.txt
$ pip install uwsgi
$ chmod 750 start.sh stop.sh
$ ./start.sh
$ ps -aux | grep uwsgi -
测试项目
本地访问你的域名即可;如果 502,按「uWSGI 是否在跑 →
nginx -t→ 错误日志 → 静态目录权限」的顺序查。
三、推荐路线:Gunicorn + systemd + Nginx(全部实测)
这一节每一步都在 WSL2 上真跑过,输出贴在对应位置。
目录与虚拟环境
1 | /srv/django/probe/ # 项目根 |
1 | $ python3 -m venv /srv/django/venv |
Gunicorn 配置文件
1 | bind = "127.0.0.1:8000" |
启动后的实测日志:
1 | [INFO] Starting gunicorn 26.2.0 |
accesslog = "-" 表示把访问日志写到 stdout,交给 systemd 收集(见 3.4)。
systemd unit
1 | [Unit] |
实测行为:
1 | systemctl start djdeploy -> active,Main PID: 4618 (gunicorn) |
Type=notify 让 systemd 等 gunicorn 真正就绪才算启动成功;Restart=always 是崩溃自愈的关键;EnvironmentFile 放密钥(权限 600),不要把 SECRET_KEY 写进 unit 文件。
改代码后不中断服务:
1 | $ sudo systemctl reload djdeploy # 内部就是 kill -HUP 主进程 |
实测 HUP 后主进程 pid 不变、worker 全换:
1 | [INFO] Worker exiting (pid: 4208) |
日志交给 journald
1 | $ journalctl -u djdeploy --no-pager -n 6 |
访问日志、异常 traceback、systemd 的启停记录都在一处,比 daemonize=uwsgi.log 好查。
Nginx 反代
1 | upstream django-app { |
实测:
1 | /static/site.css -> 200 165B 0.0024s |
从宝塔 + uWSGI 迁过来的注意点
uwsgi.ini里的processes/threads换成workers(Gunicorn 的 sync worker 模型没有线程维度,要线程用gthread)。- Nginx 的
uwsgi_pass+include uwsgi_params换成proxy_pass,协议从 uWSGI 变成 HTTP。 start.sh/stop.sh删掉,换成systemctl start/stop/reload。daemonize日志文件删掉,改accesslog="-"交给 journald。- 面板里保留站点、SSL、Nginx 配置,只把「Python 项目」那一层换掉。
四、异步形态:Uvicorn(实测)
用到异步视图、WebSocket、async 相关能力时,ASGI 入口换成 uvicorn:
1 | $ uvicorn proj.asgi:application --host 127.0.0.1 --port 8100 --workers 2 |
实测并发(/slow/ 固定 0.2s):
1 | uvicorn 2 worker 10 个并发墙钟 0.25s |
单 worker 也能并发,是因为 Django 的同步视图在 ASGI 下由 asgiref 丢进线程池执行(实测那个进程里有 5 个线程)。两个连带影响:并发上限变成「worker × 线程池」,数据库连接数要重新算;另外 uvicorn 0.54.0 没有 --daemon 和 --pid-file 选项,后台运行请交给 systemd:
1 | Error: No such option '--pid-file'. (Did you mean one of: '--env-file', '--ssl-keyfile'?) |
也可以两者结合:Gunicorn 主进程管 worker,worker 用 uvicorn 的实现,在 gunicorn.conf.py 里写 worker_class = "uvicorn.workers.UvicornWorker"。
五、容器化部署(官方文档步骤,未在本机验证)
本机没有 Docker 运行时,下面这段没有实测,步骤以官方文档为准,端口和路径按你的项目改。
Dockerfile
1 | FROM python:3.12-slim |
PYTHONUNBUFFERED=1 很关键,否则日志会被缓冲,docker logs 半天看不到东西。
docker-compose
1 | services: |
要点:
- 静态资源用命名卷在
web与nginx之间共享,web启动前跑collectstatic。 - 迁移放在发布流程里单独执行(
docker compose run web python manage.py migrate),不要塞进容器的启动命令,否则多副本会同时迁移。 - 密钥用
env_file/ secrets,不要写进镜像层。 - 上传目录同样挂卷或换对象存储,否则容器重建即丢。
常见坑与排查顺序
| 现象 | 先看什么 | 实测/依据 |
|---|---|---|
| 502 Bad Gateway | 后端进程在不在、nginx -t、错误日志 |
实测后端全停时 no live upstreams |
| 400 Bad Request | ALLOWED_HOSTS 有没有当前 Host |
实测不在名单里直接 400 |
| 静态 404 | collectstatic 跑没跑、alias 路径与权限 |
实测哈希名 site.b9bcd8def6c5.css |
| 上传文件 413 | Nginx client_max_body_size(默认 1m) |
与 Django 的两个 *_MAX_MEMORY_SIZE 是不同层 |
| 页面时好时坏 | 多实例是否共享 session/缓存 | 实测 locmem 每个 worker 一份 |
| 时间差 8 小时 | 是否用数据库 Now() 写时间列 |
实测 MySQL 会话时区 SYSTEM |
| 改了代码没生效 | 有没有 reload(uWSGI/Gunicorn 都要重启 worker) | 实测 HUP 后 worker 换新 pid |
| 端口暴露风险 | socket/bind 是否写了 0.0.0.0 |
老笔记的 socket=0.0.0.0:8000 是隐患 |
排查顺序建议固定成四步:进程活着吗 → 端口监听在哪个地址 → 后端直连能不能通 → Nginx 错误日志怎么说。
1 | $ systemctl status djdeploy |
参考
- Django 官方:部署到 uWSGI:
uwsgi.ini各参数的官方含义。 - Django 官方:部署清单:DEBUG、ALLOWED_HOSTS、静态与密钥。
- Gunicorn 官方文档:配置文件项、
Type=notify的 systemd 集成、优雅重启。 - Uvicorn 官方设置项:
--workers、--proxy-headers、--forwarded-allow-ips。 - Nginx
uwsgi模块 与proxy_pass:两种协议对接的区别。 - systemd.service 手册:
Type=notify、ExecReload、EnvironmentFile。 - Docker 官方:Django 最佳实践:镜像、卷与迁移的编排方式(本机未验证)。