1. 项目概述:为什么一个 Django 应用必须认真对待“尺寸”与“防护”
你写完一个 Django 项目,本地
python manage.py runserver
跑得飞快,API 返回秒级响应,前端页面加载丝滑——这很美好。但当你把它扔进生产环境,面对真实用户、真实流量、真实攻击者时,这种“美好”往往在三分钟内崩塌:CPU 突然飙到 98%,Nginx 返回 502 Bad Gateway,Let’s Encrypt 自动续期失败导致全站 HTTPS 中断,Docker 容器反复重启,日志里满屏
Connection refused
和
timeout
。这不是玄学,是典型的“未维度化 + 未防护”组合拳打出来的结果。
所谓 dimensionner (法语,意为“确定规模、设定规格”),在 Django 生产部署中,绝不是简单地选台 4 核 8G 的云服务器就完事。它是一套系统性工程判断:你的应用每秒要处理多少并发请求?每个请求平均消耗多少内存?数据库连接池该开多大?Docker 容器的 CPU 限额和内存限制设多少才既防 OOM 又不浪费资源?静态文件缓存策略怎么配才能让 Nginx 真正扛住流量?这些参数不是拍脑袋定的,而是基于压测数据、资源监控曲线和业务增长模型推演出来的。我见过太多团队,把开发机配置直接照搬到生产,结果上线三天就扩容两次,每次都是半夜被告警电话叫醒。
而
sécuriser
(防护),更不是加个
SECURE_SSL_REDIRECT = True
就高枕无忧。它是一道立体防线:Docker 层面要禁用特权模式、挂载只读文件系统、使用非 root 用户运行;Nginx 层面要过滤恶意 User-Agent、限制请求频率、隐藏版本号、强制 HSTS;Let’s Encrypt 不只是“装上证书”,更要确保 ACME 协议通信走专用端口、私钥权限严格为
600
、续期脚本有失败回滚机制;Django 层面则要关闭调试模式、设置
ALLOWED_HOSTS
为精确域名列表、启用
X-Content-Type-Options
等安全头。任何一环松动,都可能成为攻击入口。
这个标题直指现代 Web 应用交付的核心矛盾:
功能交付速度 vs. 系统稳定性边界
。它面向的不是刚学完
pip install django
的新手,而是已经能写出完整 CRUD、正准备把第一个真实项目推向用户的中级开发者,或是负责技术选型、需要向运维/架构团队解释方案合理性的后端负责人。你不需要从零造轮子,但必须清楚每个组件在整条链路上承担什么职责、暴露什么风险、又如何协同加固。接下来的内容,就是我过去三年在电商后台、SaaS 平台、教育管理系统等十余个 Django 生产项目中,踩过坑、调过参、熬过夜后沉淀下来的实操手册——没有理论堆砌,只有可抄、可改、可验证的具体步骤和参数依据。
2. 整体架构设计与核心组件选型逻辑
2.1 为什么是 Docker + Nginx + Let’s Encrypt 这个铁三角?
先说结论:这不是为了“时髦”,而是当前 Linux 服务器环境下, 成本、安全、可维护性、自动化程度四者平衡后的最优解 。我们来逐层拆解这个选择背后的硬逻辑。
Docker 的核心价值,在于
环境隔离与部署一致性
。Django 项目依赖 Python 版本、特定 C 库(如
libpq
用于 PostgreSQL)、编译型依赖(如
Pillow
的
libjpeg
)。在 Ubuntu 22.04 上 pip install 成功的包,在 CentOS 7 上可能因 glibc 版本差异直接编译失败。Docker 通过镜像固化整个运行时环境,让
docker build
产出的镜像,在开发机、测试机、生产机上行为完全一致。更重要的是,它天然支持
资源限制
:你可以用
--memory=512m --cpus=1.5
精确控制容器能使用的最大内存和 CPU 时间片,这是传统虚拟机或裸机部署无法低成本实现的。我曾用
docker stats
监控一个 Django API 容器,发现其内存占用在 320MB~480MB 波动,于是将
--memory
设为
512m
,再配合
--memory-swap=512m
(禁止使用 swap),彻底杜绝了因内存溢出触发 OOM Killer 杀死进程的风险。
Nginx 在这里扮演
反向代理与边缘网关
的双重角色。很多人误以为它只是“转发请求”,其实它的关键能力在于:
连接管理
。Django 的
runserver
是单线程阻塞式,Gunicorn/uWSGI 是多进程/多线程模型,但它们都受限于 Python GIL 和同步 I/O,难以高效处理成千上万的长连接(如 WebSocket、SSE)。Nginx 基于事件驱动(epoll/kqueue),用极小的内存开销就能维持数万并发连接。它把海量客户端连接“接住”,再以可控的并发数(通过
upstream
的
max_conns
和
keepalive
参数)转发给后端 Django 应用。同时,Nginx 天然承担了
静态文件服务、SSL 终结、请求过滤、负载均衡(未来扩展)
等职责,让 Django 专注业务逻辑,不必再为
collectstatic
后的文件分发、HTTPS 握手耗时等问题分心。
Let’s Encrypt 则解决了
HTTPS 普及化的最后一公里障碍
。过去自签名证书或商业 CA 证书,要么不被浏览器信任,要么年费高昂、流程繁琐。Let’s Encrypt 提供免费、自动化、可信的 X.509 证书,其 ACME 协议设计精巧:客户端(如 Certbot)只需证明你对域名拥有控制权(通过 HTTP-01 或 DNS-01 挑战),即可自动签发和续期。关键在于,
它与 Nginx 的集成是开箱即用的
。Certbot 能自动修改 Nginx 配置,添加临时 location 块来响应 ACME 挑战,并在签发成功后无缝切换到 HTTPS 配置。这使得“全站 HTTPS”从一个需要专人维护的复杂任务,变成了一个
certbot --nginx -d example.com
命令就能完成的标准化操作。
提示:这个组合的“不可替代性”体现在故障隔离上。当 Django 应用因代码 bug 崩溃时,Nginx 会返回 502,但自身依然健壮,用户看到的是友好的错误页而非空白;当 Let’s Encrypt 续期失败,Nginx 仍可用旧证书提供 HTTPS 服务,给你留出修复时间;Docker 容器崩溃,
docker restart一条命令即可恢复,无需登录服务器手动启停进程。三者各司其职,互为备份。
2.2 架构拓扑图与数据流向详解
虽然不能画 Mermaid 图,但我用文字精准描述这个生产环境的标准拓扑:
Internet
↓ (HTTPS:443 / HTTP:80)
[Public IP] → [Nginx (Host Network)]
↓ (HTTP:8000, via Docker bridge network 'webnet')
[Django App Container (Gunicorn)] ←→ [PostgreSQL Container]
↓ (HTTP:8000, same network)
[Static Files: /static/ → Nginx volume mount]
↓ (HTTP:8000, same network)
[Media Files: /media/ → Nginx volume mount, with auth if needed]
-
网络层面
:Nginx 运行在宿主机的
host网络模式,直接监听0.0.0.0:80和0.0.0.0:443。这是必须的,因为 Let’s Encrypt 的 ACME 挑战需要从公网直接访问 Nginx 的 80 端口。而 Django 容器、PostgreSQL 容器则运行在一个名为webnet的自定义 Docker bridge 网络中,它们之间通过容器名(如web、db)互相解析,完全隔离于公网。 -
数据流向
:
-
用户访问
https://example.com/api/v1/users/,DNS 解析到服务器公网 IP。 -
请求抵达 Nginx,Nginx 根据
server_name example.com匹配到 HTTPS server 块。 -
Nginx 将请求通过
proxy_pass http://web:8000;转发给web容器的 8000 端口(Gunicorn 监听端口)。 -
Django 处理请求,若需查库,则通过
DATABASE_URL=postgresql://user:pass@db:5432/mydb连接同网络下的db容器。 - Django 返回 HTML 或 JSON,Nginx 将其作为响应体发回用户。
-
用户访问
https://example.com/static/css/app.css,Nginx 直接从./staticfiles/目录(通过volumes挂载)读取文件并返回, 完全不经过 Django ,这是性能关键。
-
用户访问
-
安全边界
:防火墙(如
ufw)只开放22(SSH)、80(HTTP,用于 ACME)、443(HTTPS)三个端口。Docker 容器的内部端口(如8000,5432)对公网完全不可见,只能被同网络的其他容器访问。
2.3 为什么不用 Apache?为什么不用 Traefik?
Apache 是老牌 Web 服务器,但它在现代容器化场景下有两个硬伤:
内存开销大、配置复杂度高
。Apache 的 prefork MPM 模型为每个请求 fork 一个新进程,内存占用随并发线性增长。一个空载的 Apache 进程常驻内存约 10MB,而 Nginx worker 进程仅需 2~3MB。在资源有限的云服务器上,这决定了你能承载的并发上限。更重要的是,Apache 的
.htaccess
机制允许目录级重写,这在 Docker 容器里意味着你需要把配置文件打入镜像或通过卷挂载,破坏了镜像的不可变性原则。Nginx 的配置是集中式的、声明式的,所有规则都在
/etc/nginx/conf.d/
下,易于版本控制和 CI/CD 流水线注入。
Traefik 是一个现代化的反向代理,主打“自动服务发现”。它能监听 Docker daemon,当新容器启动时自动为其生成路由规则。听起来很酷,但对 Django 项目而言,它带来了不必要的复杂性。Traefik 的配置学习曲线陡峭,其 ACME 集成虽好,但一旦出现证书问题,排查路径远比直接看 Nginx 日志和 Certbot 输出要长。更重要的是,
Traefik 的定位是“微服务网关”,而 Django 通常是一个单体应用(Monolith)
。为一个单体应用引入一个专为微服务设计的网关,属于典型的“杀鸡用牛刀”。Nginx 配置清晰、文档丰富、社区支持强大,一个
nginx.conf
文件就能搞定所有需求,这才是务实之选。
3. 核心细节解析与实操要点
3.1 Django 应用的“尺寸”量化:从代码到资源的全链路估算
“Dimensionner”不是玄学,是可量化的工程活动。我总结了一套适用于 Django 的四步估算法,已在多个项目中验证有效。
第一步:估算并发请求数(QPS)
这是所有后续计算的基石。公式很简单:
QPS = (日活用户数 × 日均访问次数) / (24 × 3600)
。但要注意“访问次数”的定义。对于一个电商后台 API,一个用户一次“下单”操作可能触发 5~8 个独立 API 请求(获取地址、校验库存、创建订单、扣减积分、发送通知)。所以,如果你的后台日活是 1000 人,每人每天平均完成 3 笔订单,那么 QPS ≈
(1000 × 3 × 6) / 86400 ≈ 0.21
。这看起来很低,但这是“平均值”,峰值 QPS 可能是均值的 3~5 倍(例如大促期间),所以按
0.21 × 5 = 1.05
QPS 作为基准设计目标。记住,
永远按峰值设计,而不是平均值
。
第二步:估算单请求资源消耗
这需要实测。我在本地用
locust
做压测,模拟 100 个并发用户,持续 5 分钟,观察
docker stats web
输出:
- CPU%:稳定在 15%~25%
- MEM USAGE / LIMIT:380MiB / 512MiB
-
NET I/O:12MB / 5min
这意味着,在 100 并发下,单个请求平均消耗
380MiB / 100 = 3.8MiB内存。但这只是瞬时内存,还要考虑 Python 的垃圾回收延迟。更稳妥的做法是看MEM USAGE的峰值,比如压测中最高冲到450MiB,那么单请求内存预算应设为450MiB / 100 = 4.5MiB。
第三步:计算 Gunicorn 工作进程数
Gunicorn 的
--workers
参数不是越多越好。经验公式:
workers = (2 × CPU_cores) + 1
。但这是针对 CPU 密集型任务。Django 是 I/O 密集型,更多瓶颈在数据库和网络。我的实践是:
先设为
3
,然后根据
docker stats
的 CPU% 和
gunicorn
日志中的
worker timeout
频率来动态调整
。如果 CPU% 长期低于 30%,且无 timeout,说明 worker 过多,浪费内存;如果 CPU% 频繁飙到 90%+,且日志里大量
Worker exiting after 30 seconds
,说明 worker 不足,需要增加。我最终在一个 2 核服务器上,将
--workers
设为
4
,
--worker-class gevent
(异步模型),
--worker-connections 1000
,达到了最佳平衡。
第四步:确定 Docker 资源限制
基于第二步的实测数据,我们得出单请求内存约 4.5MiB。假设目标峰值 QPS 是 10,那么理论内存需求是
10 × 4.5MiB = 45MiB
。但这忽略了操作系统、Gunicorn 主进程、Python 解释器本身的开销。我通常在此基础上乘以 8~10 倍的安全系数。所以
45MiB × 10 = 450MiB
,向上取整为
512MiB
。CPU 限制同理:压测时 CPU% 最高为 25%,那么
--cpus=0.5
(即 50% 的一个 CPU 核心)就足够应对 10 QPS。最终的
docker run
命令片段如下:
docker run \
--name web \
--network webnet \
--memory=512m \
--memory-swap=512m \
--cpus=0.5 \
--restart=unless-stopped \
-e DJANGO_SETTINGS_MODULE=myproject.settings.production \
-v /path/to/staticfiles:/app/staticfiles:ro \
-v /path/to/media:/app/media:rw \
my-django-app:latest
注意:
--memory-swap=512m是关键。它等于--memory,意味着完全禁用 swap。Linux 的 OOM Killer 在内存不足时,会优先杀死占用内存最多的进程。如果允许 swap,Docker 容器可能会因频繁 swap I/O 而卡死,导致整个系统响应迟钝。禁用 swap 强制容器在内存耗尽时被 OOM Killer 杀死,然后由--restart=unless-stopped策略自动拉起一个干净的新实例,这是一种更优雅的“故障自愈”。
3.2 Nginx 配置的“防护”细节:超越基础转发的 7 个安全加固点
一个默认的
nginx.conf
只是“能用”,离“安全”还很远。以下是我在生产环境中必配的 7 个加固点,每一个都有明确的攻击场景对应。
1. 隐藏 Nginx 版本号
# 在 http 块中
server_tokens off;
为什么?暴露
Server: nginx/1.18.0
会让攻击者立刻知道你的软件版本,从而精准搜索已知 CVE(如 CVE-2021-23017)。关闭后,Header 中的
Server
字段将变为
Server: nginx
,增加了攻击者的信息收集成本。
2. 强制 HSTS(HTTP Strict Transport Security)
# 在 server { listen 443 ssl; } 块中
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
HSTS 告诉浏览器:“未来一年内,所有对该域名的请求,必须用 HTTPS 发起,即使用户手动输入
http://
,浏览器也会自动跳转”。这彻底杜绝了 SSL Stripping(降级攻击)的可能性。
includeSubDomains
表示子域名也生效,
preload
表示申请加入浏览器内置的 HSTS Preload List,这是最高级别的保护。
3. 防止 MIME 类型混淆攻击
add_header X-Content-Type-Options "nosniff" always;
攻击者可能上传一个名为
image.jpg
但实际内容是 JavaScript 的文件,诱导浏览器执行。
nosniff
指令强制浏览器严格按照
Content-Type
Header 解析,不进行 MIME 类型嗅探。
4. 防止点击劫持(Clickjacking)
add_header X-Frame-Options "DENY" always;
# 或者更灵活的
# add_header X-Frame-Options "SAMEORIGIN" always;
DENY
表示该页面绝对不允许被任何
<frame>
、
<iframe>
加载,彻底杜绝点击劫持。
SAMEORIGIN
允许同源 iframe 加载,适用于需要嵌入到自己其他站点的场景。
5. 限制请求体大小与超时
# 在 http 块中,全局设置
client_max_body_size 20M;
client_header_timeout 10;
client_body_timeout 10;
send_timeout 10;
# 在 server 块中,针对 Django API
location /api/ {
proxy_read_timeout 300; # Django 视图可能需要长时间处理
proxy_send_timeout 300;
}
client_max_body_size
防止攻击者上传超大文件耗尽磁盘空间。
*_timeout
参数防止慢速攻击(Slowloris),即攻击者建立大量连接但只发送部分 Header,让服务器连接池被占满。
proxy_read_timeout
则给 Django 留出足够的处理时间,避免 Nginx 过早中断长耗时请求。
6. 过滤恶意 User-Agent 和 Referer
# 在 http 块中
map $http_user_agent $blocked_ua {
default 0;
"~*sqlmap" 1;
"~*nikto" 1;
"~*nmap" 1;
"~*wget" 1;
}
map $http_referer $blocked_ref {
default 0;
"~*semalt.com" 1;
"~*buttons-for-website.com" 1;
}
# 在 server 块中
if ($blocked_ua) { return 403; }
if ($blocked_ref) { return 403; }
这是一个轻量级的 WAF(Web Application Firewall)雏形。通过
map
指令预编译正则表达式,性能极高。它能拦截常见的扫描器(sqlmap, nikto)和垃圾流量来源(semalt),日志中
403
错误的突增,往往是大规模扫描的前兆。
7. 静态文件的精细权限控制
location /static/ {
alias /app/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
# 禁止执行任何脚本
location ~ \.(php|py|pl|sh|cgi)$ {
deny all;
}
}
alias
比
root
更安全,因为它不会拼接路径。
expires 1y
和
immutable
让浏览器长期缓存,极大减轻后端压力。最关键的是内部的
location ~ \.(php|py|...)$
,它确保即使攻击者设法在
static/
目录下上传了一个
shell.php
,Nginx 也会直接
deny all
,绝不交给任何后端处理器执行。
3.3 Let’s Encrypt 的自动化与可靠性保障:不止于
certbot --nginx
Let’s Encrypt 的魅力在于自动化,但自动化背后是严谨的可靠性设计。我从不信任
certbot renew --dry-run
的输出,而是构建了一套“三重保险”机制。
第一重保险:ACME 协议通信的专用端口
默认情况下,Certbot 使用
http-01
挑战,需要监听
80
端口。但很多企业防火墙会屏蔽
80
端口,或者你已有其他服务在用
80
。解决方案是使用
tls-alpn-01
挑战,它通过
443
端口的 TLS ALPN 扩展完成验证。这要求 Nginx 配置支持 ALPN:
# 在 server { listen 443 ssl; } 块中,添加
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# Certbot 会自动配置 ALPN,无需额外 Nginx 设置
然后运行:
certbot certonly --standalone --preferred-challenges tls-alpn-01 -d example.com
--standalone
模式会启动一个临时的、最小化的 Web 服务器来响应挑战,完全不依赖 Nginx,规避了端口冲突。
第二重保险:证书续期的原子化与回滚
certbot renew
默认是“尽力而为”,失败了也不会通知你。我将其包装成一个带事务语义的脚本:
#!/bin/bash
# /usr/local/bin/renew-cert.sh
set -e # 任何命令失败,立即退出
# 1. 创建备份
cp /etc/letsencrypt/live/example.com/fullchain.pem /etc/letsencrypt/live/example.com/fullchain.pem.bak
cp /etc/letsencrypt/live/example.com/privkey.pem /etc/letsencrypt/live/example.com/privkey.pem.bak
# 2. 执行续期
certbot renew --quiet --no-self-upgrade --post-hook "/bin/systemctl reload nginx"
# 3. 验证新证书是否有效
if ! openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -checkend 86400; then
echo "New cert will expire in less than 24h! Restoring backup..."
cp /etc/letsencrypt/live/example.com/fullchain.pem.bak /etc/letsencrypt/live/example.com/fullchain.pem
cp /etc/letsencrypt/live/example.com/privkey.pem.bak /etc/letsencrypt/live/example.com/privkey.pem
systemctl reload nginx
exit 1
fi
set -e
确保脚本在任何一步失败时终止。
--post-hook
在续期成功后自动重载 Nginx。最关键的是第 3 步,用
openssl x509 -checkend
检查新证书的有效期是否大于 24 小时,如果不是(意味着续期失败但 Certbot 没报错),则立即回滚到备份证书并重载 Nginx,保证服务不中断。
第三重保险:监控与告警
我用
cron
每天凌晨 2 点执行续期脚本,并将输出重定向到日志:
0 2 * * * /usr/local/bin/renew-cert.sh >> /var/log/cert-renew.log 2>&1
然后配置一个简单的日志监控:
# 检查日志中最近 24 小时是否有 "ERROR" 或 "failed"
if grep -q "ERROR\|failed" /var/log/cert-renew.log | tail -n 100; then
echo "Cert renewal failed! Check logs." | mail -s "ALERT: Cert Renewal Failed" admin@example.com
fi
这构成了从“自动执行”到“自动验证”再到“自动告警”的完整闭环。
4. 实操过程与核心环节实现
4.1 从零开始:Dockerfile 的精益编写与多阶段构建
一个糟糕的
Dockerfile
会让镜像臃肿、启动缓慢、存在安全风险。我坚持“最小化、分层化、可复现”三大原则。
基础镜像选择
# 第一阶段:构建阶段
FROM python:3.11-slim-bookworm AS builder
# 安装构建依赖(仅在构建阶段需要)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# 复制 requirements.txt 并安装依赖
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# 第二阶段:运行阶段
FROM python:3.11-slim-bookworm
# 创建非 root 用户
RUN addgroup -g 1001 -f appgroup && adduser -S appuser -u 1001
# 复制第一阶段构建好的 wheels 和依赖
COPY --from=builder /app/wheels /wheels
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
# 复制应用代码
COPY . .
# 切换到非 root 用户
USER appuser
# 暴露端口
EXPOSE 8000
# 启动命令
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--worker-class", "gevent", "--timeout", "120", "myproject.wsgi:application"]
-
为什么用
slim-bookworm?bookworm是 Debian 12,比bullseye(Debian 11)更新,安全补丁更及时。slim镜像去除了apt、vim等非必要工具,基础镜像大小仅 120MB,远小于full镜像的 900MB。 -
多阶段构建的意义
:第一阶段安装了
gcc和libpq-dev来编译psycopg2-binary等 C 扩展,但这些编译工具在运行时完全不需要。第二阶段直接复制编译好的 wheel 包,最终镜像里只有 Python 解释器、依赖包和你的代码,体积更小,攻击面更窄。 -
非 root 用户
:
USER appuser是强制要求。Docker 容器默认以 root 运行,一旦容器被攻破,攻击者就获得了宿主机的 root 权限。创建一个 UID 为 1001 的普通用户,是纵深防御的第一道门槛。 -
--timeout 120:Gunicorn 默认超时是 30 秒,但对于涉及复杂计算或外部 API 调用的视图,30 秒太短,容易导致 Nginx 报 504。120 秒是一个更合理的默认值,你可以在具体视图中用@time_limit装饰器做更细粒度的控制。
构建与推送
# 构建镜像,打上 git commit hash 标签,确保可追溯
docker build -t my-django-app:$(git rev-parse --short HEAD) .
# 推送到私有仓库(如 Docker Hub, GitLab Registry)
docker tag my-django-app:$(git rev-parse --short HEAD) registry.example.com/my-django-app:$(git rev-parse --short HEAD)
docker push registry.example.com/my-django-app:$(git rev-parse --short HEAD)
4.2 docker-compose.yml:生产就绪的编排蓝图
docker-compose.yml
是整个部署的“宪法”,它定义了所有服务的依赖、网络、卷和健康检查。
version: '3.8'
# 定义自定义网络,确保服务间通信安全
networks:
webnet:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
# 定义服务
services:
# Django 应用服务
web:
image: registry.example.com/my-django-app:latest
restart: unless-stopped
networks:
- webnet
volumes:
- ./staticfiles:/app/staticfiles:ro
- ./media:/app/media:rw
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
environment:
- DJANGO_SETTINGS_MODULE=myproject.settings.production
- DATABASE_URL=postgresql://user:password@db:5432/mydb
- REDIS_URL=redis://cache:6379/1
- SECRET_KEY=${SECRET_KEY}
- DEBUG=False
# 关键:健康检查,让 Docker 知道服务是否真正就绪
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# PostgreSQL 数据库
db:
image: postgres:15-alpine
restart: unless-stopped
networks:
- webnet
volumes:
- ./postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=mydb
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
# 数据库健康检查
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 30s
timeout: 10s
retries: 3
# Redis 缓存(可选,但强烈推荐)
cache:
image: redis:7-alpine
restart: unless-stopped
networks:
- webnet
command: redis-server --appendonly yes
volumes:
- ./redis-data:/data
# Nginx 反向代理
nginx:
image: nginx:1.25-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
networks:
- webnet
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./staticfiles:/app/staticfiles:ro
- ./media:/app/media:ro
- /etc/letsencrypt:/etc/letsencrypt:rw
- /var/log/nginx:/var/log/nginx:rw
depends_on:
web:
condition: service_healthy
db:
condition: service_healthy
-
healthcheck是灵魂 :depends_on默认只检查容器是否started,但 Django 应用启动后,还需要时间加载模块、连接数据库、初始化缓存。service_healthy条件强制 Nginx 必须等到web服务的/health/接口返回 200 才启动,避免了“502 Bad Gateway”的尴尬。 -
volumes的读写权限 :staticfiles对 Nginx 是ro(只读),因为它是构建时生成的,运行时不应被修改;media对 Nginx 是ro,但对web是rw,因为用户上传的文件需要 Django 写入;/etc/letsencrypt对 Nginx 是rw,因为 Certbot 需要写入证书。 -
start_period:给新启动的容器一个“宽限期”,让它有足够时间完成初始化,再开始健康检查。40 秒是 Django 应用在冷启动时的典型耗时。
4.3 Nginx 配置文件详解:production.conf 的逐行注释
这是
./nginx/conf.d/production.conf
的完整内容,每一行都经过深思熟虑:
# 1. HTTP 重定向:所有 HTTP 请求强制跳转到 HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
# 2. HTTPS 主服务块
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL 证书配置
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# SSL 性能与安全优化
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 安全头加固(前面已详述,此处省略重复说明)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add
362

被折叠的 条评论
为什么被折叠?



