功能已经比较完整,最后把项目放进真实运行环境。部署要收口开发阶段的假设。DEBUG 要关掉,密钥要交给环境,静态文件要有出口,进程要有健康检查。

公共请求先到 Nginx,TLS 和静态文件在这里结束;动态请求进入 Gunicorn,再交给 Django。应用数据、附件对象和缓存分开存放,任何一块出问题都不会把所有状态混在一起。
让配置跟着环境走
开发时保留 DEBUG=True 很方便,生产环境必须显式关闭。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| import os
SECRET_KEY = os.environ.get(
"DJANGO_SECRET_KEY",
"django-insecure-local-development-key",
)
DEBUG = os.environ.get(
"DJANGO_DEBUG",
"true",
).lower() in {"1", "true", "yes"}
ALLOWED_HOSTS = [
host
for host in os.environ.get(
"DJANGO_ALLOWED_HOSTS",
"localhost,127.0.0.1",
).split(",")
if host
]
|
默认值只服务本地开发。生产容器必须注入强随机 DJANGO_SECRET_KEY,并把真实域名放进 DJANGO_ALLOWED_HOSTS。
继续加入 CSRF 来源、静态根目录和非 DEBUG 下的安全项。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| STATIC_ROOT = BASE_DIR / "staticfiles"
CSRF_TRUSTED_ORIGINS = [
origin
for origin in os.environ.get(
"DJANGO_CSRF_TRUSTED_ORIGINS",
"",
).split(",")
if origin
]
if not DEBUG:
SECURE_SSL_REDIRECT = os.environ.get(
"DJANGO_SECURE_SSL_REDIRECT",
"true",
).lower() == "true"
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
|
STATIC_ROOT 是 collectstatic 的目标目录。STATICFILES_DIRS 则指向项目里的源静态目录;前者存收集结果,后者存源文件。
Django 6.1 的 MAILERS 也要随环境变化。开发用 console,生产切换到 SMTP。
1
2
3
4
5
6
7
8
9
| MAILERS = {
"default": {
"BACKEND": (
"django.core.mail.backends.smtp.EmailBackend"
if not DEBUG
else "django.core.mail.backends.console.EmailBackend"
),
},
}
|
DJANGO_DEBUG=false 后再执行部署检查,就能看到当前还有哪些安全项没收好。
1
| uv run manage.py check --deploy
|
本项目的检查结果通过。
1
| System check identified no issues (0 silenced).
|
健康检查尽量小
负载均衡和容器平台需要知道进程是否活着。给应用加一个最小接口。
1
2
3
4
| from django.http import JsonResponse
def health(request):
return JsonResponse({"status": "ok"})
|
根路由这样挂。
1
2
3
4
5
6
| from health import health
urlpatterns = [
path("healthz/", health, name="health"),
# ...
]
|
健康接口不要查数据库、Redis 和 OSS。它回答的是 WSGI 进程能否响应请求。如果把这些依赖都塞进去,某个下游短暂变慢时,负载均衡可能把本来还能服务的应用全部摘掉。
测试写成下面这样。
1
2
3
4
5
6
7
8
| from django.test import TestCase
from django.urls import reverse
class HealthTests(TestCase):
def test_healthz_returns_ok(self):
response = self.client.get(reverse("health"))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"status": "ok"})
|
真实浏览器访问 /healthz/,返回 JSON。

收集静态文件
生产模板里仍然写 {% static %},但静态文件最终要进入 STATIC_ROOT。
1
| uv run manage.py collectstatic --noinput
|
本阶段收集了 132 个文件,包括项目 CSS 和 Django Admin 静态资源。
1
| 132 static files copied to '.../source/staticfiles'.
|
Nginx 直接服务这些文件,不要让 Gunicorn 每次都读 CSS、JS 和图标。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| server {
listen 80;
server_name blog.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name blog.example.com;
ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;
client_max_body_size 20m;
location /static/ {
alias /opt/blog/source/staticfiles/;
expires 30d;
access_log off;
}
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;
}
}
|
client_max_body_size 要大于图片上传限制。否则用户上传大图时,Nginx 会先返回 413,Django 根本没机会给出友好提示。
用 Gunicorn 运行
Gunicorn 加入依赖。
本阶段安装的版本如下。
启动命令如下。
1
2
3
| uv run gunicorn config.wsgi:application \
--bind 127.0.0.1:8000 \
--workers 3
|
Gunicorn 在 Windows 上不能运行,这是正常现象。它依赖 Unix 的 fcntl,部署目标是 Linux 服务器或容器。开发与测试仍在 Windows 上使用 runserver。
进程数不要拍脑袋。小型博客可以从 3 个 worker 开始,观察 CPU、内存和响应时间。worker 太少会排队,太多会挤占 SQLite 或 PostgreSQL 的连接能力。
用容器固定运行环境
source/Dockerfile 使用 uv 和 Python 3.14 基础镜像。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| FROM ghcr.io/astral-sh/uv:python3.14-bookworm-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_COMPILE_BYTECODE=1
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
COPY . .
RUN uv run python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["uv", "run", "--no-sync", "gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
|
--frozen 保证容器只用 uv.lock 中锁定的版本,不会在构建时意外解析出新包。构建阶段先复制依赖清单,再复制源码,能更好利用镜像层缓存。
环境变量放进 source/.env.example,真实值写在本机 .env。
1
2
3
4
5
6
| DJANGO_DEBUG=false
DJANGO_SECRET_KEY=change-me-with-64-random-characters
DJANGO_ALLOWED_HOSTS=blog.example.com
DJANGO_CSRF_TRUSTED_ORIGINS=https://blog.example.com
DJANGO_SECURE_SSL_REDIRECT=true
REDIS_URL=redis://127.0.0.1:6379/0
|
OSS 的五个变量也在里面。密钥不要写进源码,也不要复制进聊天记录。
数据库迁移要分两步看
部署前先检查迁移是否遗漏。
1
| uv run manage.py makemigrations --check --dry-run
|
如果输出 No changes detected,说明模型和迁移一致。
然后在发布窗口执行下面这条命令。
1
| uv run manage.py migrate
|
新迁移要先能兼容旧代码,再发布新代码;删除列、收紧约束这类不可逆变更要更谨慎。可以先把数据库结构准备好,再让新版本上线,旧代码在过渡期仍然能运行。
发布清单
这次收尾检查了五个部分。
1
2
3
4
5
6
7
8
| uv lock --check
uv run manage.py check
uv run manage.py makemigrations --check --dry-run
uv run manage.py test
DJANGO_DEBUG=false \
DJANGO_SECRET_KEY=... \
DJANGO_ALLOWED_HOSTS=... \
uv run manage.py check --deploy
|
最终测试结果如下。
1
2
3
| Ran 31 tests in 5.007s
OK
|
最后再收集静态文件。
1
| uv run manage.py collectstatic --noinput
|
部署配置模板也放进 source/.env.example。真实服务器只复制模板结构,不复制任何真实密钥。
部署后留下什么
开发环境可以犯错,生产环境要少给机会。DEBUG 边界、密钥来源、静态文件出口、健康检查、Gunicorn 启动方式和容器定义都已经明确。数据库、OSS 和 Redis 仍可通过环境变量替换。
源码
GitHub 地址:https://github.com/yidao620c/simpleblog