Django生产部署:Docker+Nginx+Let‘s Encrypt全栈实践
1. 项目概述:为什么一个Django应用必须走出开发服务器?
你有没有在本地用
python manage.py runserver
启动过Django项目?那个带点绿色文字、写着“Starting development server at http://127.0.0.1:8000/”的提示,亲切得像老朋友。但只要它一出现在生产环境部署文档里,我就会立刻停下——这不是启动命令,这是危险信号。Django官方文档白纸黑字写着:“
The development server is not intended to be used in a production environment.
” 它没有并发处理能力,不支持静态文件高效分发,没有请求超时控制,更别提HTTPS、负载均衡这些现代Web服务的标配功能。而标题里提到的
Docker、Nginx 和 Let's Encrypt
,恰恰就是把Django从“能跑”变成“稳跑、快跑、安全跑”的三块基石。
这个标题直指一个真实、高频、且常被低估的工程痛点: 如何让一个基于Python的Django应用,在真实互联网环境中,既具备企业级的可伸缩性(Escalar),又拥有面向公众的安全防护能力(Proteger) 。它不是教你怎么写一个API视图,而是教你如何把写好的API,真正交到用户手里,并且让用户用得放心、用得流畅。关键词 Django、Docker、Nginx、Let's Encrypt 并非简单罗列,它们构成了一条清晰的技术演进链:Django是业务逻辑的核心引擎;Docker是标准化的“运输集装箱”,确保代码在任何机器上运行效果一致;Nginx是高速路收费站兼保安队长,负责流量分发、SSL卸载、静态资源缓存;Let's Encrypt则是免费、自动、可信的“数字身份证”颁发机构,让你的网站地址栏出现那个小小的绿色锁头。这四者组合,是当前云原生时代部署Python Web应用最主流、最稳健、也最具性价比的方案。它适合所有正在将内部工具、SaaS产品或客户项目推向公网的开发者、运维工程师和小团队技术负责人——无论你用的是Ubuntu 22.04、CentOS Stream 9,还是MacBook上的Docker Desktop,这套逻辑都完全适用。它不依赖特定云厂商,不绑定昂贵商业证书,核心思想是“隔离、分层、自动化”,把复杂度关进一个个明确职责的盒子里。
2. 整体架构设计与技术选型逻辑
2.1 为什么是Docker,而不是直接在宿主机上装Python环境?
很多人会问:Django本身就是一个Python包,我直接在服务器上
pip install django
,再配个
gunicorn
不就完事了?这当然可以,而且在十年前很常见。但问题在于“可维护性”和“可预测性”。我曾经接手过一个线上项目,它的部署文档里写着“请确保系统Python版本为3.9.7,安装
psycopg2-binary==2.9.5
,并手动编译
libpq
库”。结果新同事在Ubuntu 24.04上照做,发现默认Python是3.12,
psycopg2-binary
最新版又不兼容旧版PostgreSQL驱动……一顿折腾后,他不得不降级系统Python,最后整个服务器环境变得脆弱不堪。Docker解决的正是这个问题。它把应用及其所有依赖(Python解释器、Django、数据库驱动、甚至C编译器)全部打包进一个
不可变的镜像(Immutable Image)
里。这个镜像就像一个密封的盒子,里面是什么版本、什么配置,完全由Dockerfile定义,与宿主机环境彻底解耦。你可以在本地Mac上构建一个镜像,然后把它推送到任何一台Linux服务器上运行,效果100%一致。这不仅仅是“方便”,更是
消除“在我机器上是好的”这类甩锅式问题的根本手段
。Docker的轻量级容器(Container)机制,相比传统虚拟机(VM),启动更快、资源占用更低,非常适合微服务化部署。对于Django这种IO密集型应用,一个容器跑一个Django实例,配合Nginx做反向代理,天然就形成了水平扩展的基础。
2.2 为什么Nginx是Django的“最佳拍档”,而不是Apache或Caddy?
在Web服务器选型上,Apache曾是绝对霸主,Caddy以“开箱即用HTTPS”著称。但Nginx在Django生态中胜出,核心在于其
事件驱动(Event-Driven)的异步非阻塞架构
。想象一下,你的Django应用正在处理一个需要调用外部API的请求,这个API响应慢,可能要等3秒。如果用Apache的
prefork
模式,它会为每个请求分配一个独立的进程,3秒内这个进程就被卡住了,无法处理其他请求。而Nginx不会自己去执行Django代码,它只做一件事:
高效地接收、排队、转发HTTP请求,并将响应快速返回给客户端
。它把实际的业务逻辑处理,通过
uWSGI
或
Gunicorn
协议,转交给后端的Django应用服务器。这就意味着,Nginx可以用极小的内存,同时管理成千上万个连接。它还能完美胜任静态文件(CSS、JS、图片)的直接服务,完全绕过Django的Python解释器,性能提升一个数量级。更重要的是,Nginx的配置语法清晰、模块丰富。
location
指令可以精确匹配URL路径,
upstream
块可以定义多个Django后端,实现负载均衡;
proxy_pass
指令则能无缝集成
Gunicorn
;
ssl_certificate
和
ssl_certificate_key
参数,为后续接入Let's Encrypt铺平了道路。相比之下,Apache的
.htaccess
重写规则复杂难懂,Caddy虽然自动HTTPS很酷,但在复杂的反向代理、缓存策略、日志格式定制等企业级需求上,Nginx的生态和文档成熟度仍是首选。
2.3 为什么Let's Encrypt是“免费证书”的代名词,而非自签名或商业CA?
HTTPS不是可选项,而是必选项。Chrome浏览器早已将所有HTTP网站标记为“不安全”,搜索引擎也会降低HTTP站点的排名。过去,购买一张商业SSL证书动辄每年几百上千元,对个人开发者和初创公司是不小负担。Let's Encrypt的出现,彻底改变了这一格局。它是一个由非营利组织ISRG运营的、
完全免费、自动化、开放的证书颁发机构(CA)
。它的核心价值不在于“免费”,而在于“自动化”。传统证书需要人工填写CSR(证书签名请求)、邮件验证域名、下载证书文件、再手动配置到服务器,流程繁琐且容易出错。Let's Encrypt通过ACME(Automated Certificate Management Environment)协议,让整个过程可以由脚本一键完成。你只需要告诉它你的域名,它就会自动在你的Web服务器上放置一个验证文件,或者通过DNS记录来证明你对该域名的控制权,然后自动签发并安装证书。最关键的是,它支持
90天有效期的自动续期
。这意味着,你只需在第一次部署时配置好,之后系统会定期(比如每周一次)自动检查证书剩余有效期,如果少于30天,就自动发起新的签发流程。这消除了证书过期导致网站“变红”的噩梦。选择Let's Encrypt,就是选择了“一次配置,长期无忧”的现代运维哲学。它与Nginx的结合,通过
certbot
这个官方客户端,已经形成了极其成熟的集成方案,几乎成为行业标准。
3. 核心细节解析与实操要点
3.1 Django项目本身的必要改造:从开发到生产
一个在本地
runserver
下运行良好的Django项目,要走向生产,绝不是简单换个启动命令就行。它需要几处关键的、看似微小却至关重要的改造,否则后面所有Docker和Nginx的配置都会功亏一篑。
首先,
静态文件(Static Files)的收集与服务
。开发时,Django的
runserver
会自动为你提供
/static/
路径下的CSS、JS文件。但在生产环境,我们绝不会让Django的Python进程去读取和发送这些文件,因为效率太低。正确的做法是,使用Django内置的
collectstatic
命令,将所有App里的
static
目录,以及
STATICFILES_DIRS
中定义的目录,全部“收集”(copy)到一个统一的
STATIC_ROOT
目录下。例如,在
settings.py
中,你需要这样配置:
# settings.py
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent # 注意:这里要根据你的项目结构调整,通常是manage.py的上三级
# 生产环境开关
DEBUG = False
ALLOWED_HOSTS = ['your-domain.com', 'www.your-domain.com'] # 必须明确列出,不能是['*']
# 静态文件配置
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # 这是collectstatic的目标目录
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'), # 这是你存放源静态文件的目录
]
然后,在Docker构建过程中,或者在部署脚本里,必须执行
python manage.py collectstatic --noinput
。这个命令会把所有分散的静态资源,一股脑儿地复制到
staticfiles
目录。Nginx随后就可以直接从这个目录里读取文件,而无需经过Django。
其次,
敏感信息的外部化管理
。
DEBUG=True
和
SECRET_KEY
是开发时的便利,生产时的灾难。
SECRET_KEY
一旦泄露,攻击者就能伪造session、CSRF token,危害极大。因此,绝不能把
SECRET_KEY
硬编码在
settings.py
里。标准做法是使用环境变量。在
settings.py
中,你可以这样写:
import os
from decouple import config # 需要pip install python-decouple
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
DATABASE_URL = config('DATABASE_URL')
python-decouple
库会优先从环境变量中读取,如果找不到,再尝试从项目根目录下的
.env
文件中读取。这样,你的
SECRET_KEY
就永远不会出现在Git仓库里,而是在Docker的
docker-compose.yml
中,通过
environment
字段注入,或者在服务器上通过
export SECRET_KEY=xxx
设置。
最后,
数据库连接的健壮性
。开发时用SQLite很方便,但生产环境必须用PostgreSQL或MySQL。更重要的是,Django的数据库连接池默认是关闭的,这在高并发下会导致连接数耗尽。你需要引入
django-db-geventpool
或
dj-database-url
等库,并在
DATABASES
配置中启用连接池。例如:
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.postgresql_psycopg2',
# 'NAME': 'mydb',
# 'USER': 'myuser',
# 'PASSWORD': 'mypass',
# 'HOST': 'db', # 这里指向Docker网络中的服务名
# 'PORT': '5432',
# 'OPTIONS': {
# 'MAX_CONNS': 20,
# 'MIN_CONNS': 5,
# }
# }
# }
这些改造,是整个架构稳固的根基。跳过它们,后面再强大的Docker和Nginx也无法挽救一个“先天不足”的Django应用。
3.2 Docker镜像构建:从Dockerfile到多阶段构建
构建一个生产级的Django Docker镜像,核心在于 最小化、安全化、可复现 。一个臃肿、包含大量开发依赖的镜像,不仅启动慢、占用磁盘空间大,更会带来巨大的安全风险——镜像里每一个未打补丁的软件包,都是潜在的攻击面。
第一步,选择基础镜像。我强烈推荐使用
python:3.11-slim-bookworm
。
slim
版本去掉了
apt-get
包管理器里不必要的文档、man手册和调试工具,体积比
python:3.11
基础镜像小一半以上。
bookworm
是Debian 12的代号,它比老旧的
bullseye
(Debian 11)更新,软件包更现代,安全更新更及时。绝对不要用
python:3.11-alpine
,因为Alpine Linux使用musl libc,而很多Python科学计算包(如
psycopg2
、
numpy
)在musl上编译困难,强行安装往往导致运行时崩溃。
第二步,利用
多阶段构建(Multi-stage Build)
。这是Docker最强大的特性之一,它允许你在同一个Dockerfile里定义多个构建阶段,最终只把上一阶段编译好的产物,复制到一个干净的、最小化的运行时镜像中。这对于需要编译C扩展的Python包(如
psycopg2
)尤其重要。一个典型的Dockerfile如下:
# 构建阶段:用于编译和安装所有依赖
FROM python:3.11-slim-bookworm AS builder
# 设置工作目录
WORKDIR /app
# 复制requirements.txt,先安装基础依赖(避免每次改代码都重装)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制源代码
COPY . .
# 运行阶段:一个纯净的运行时环境
FROM python:3.11-slim-bookworm
# 创建非root用户,提升安全性
RUN addgroup -g 1001 -f appgroup && adduser -S appuser -u 1001
# 设置工作目录
WORKDIR /app
# 从构建阶段复制已安装的Python包和源代码
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY --from=builder /app .
# 切换到非root用户
USER appuser
# 暴露端口
EXPOSE 8000
# 启动命令
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--access-logfile", "-", "--error-logfile", "-", "myproject.wsgi:application"]
这个Dockerfile的关键点在于:第一阶段(
AS builder
)负责安装所有东西,包括
pip install
和
collectstatic
;第二阶段则是一个全新的、空的镜像,只从第一阶段复制了
site-packages
(Python包)和源代码。最终生成的镜像里,没有
gcc
、没有
make
、没有
pip
,只有运行Django所必需的Python解释器和库。这极大地缩小了攻击面。此外,
USER appuser
强制容器以非root用户身份运行,即使应用存在漏洞,攻击者也无法获得宿主机的root权限。
3.3 Nginx配置详解:不只是反向代理
Nginx的配置文件
nginx.conf
或
/etc/nginx/sites-available/myapp
,是整个架构的“交通指挥中心”。一个错误的配置,可能导致502 Bad Gateway、静态文件404、甚至整个网站无法访问。下面是一个生产环境可用的、经过实战检验的完整配置:
# /etc/nginx/sites-available/myapp
upstream django_app {
server web:8000; # 指向Docker网络中的web服务,端口8000
}
server {
listen 80;
server_name your-domain.com www.your-domain.com;
# HTTP重定向到HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com www.your-domain.com;
# SSL证书路径(由certbot自动管理)
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
# SSL安全加固(来自Mozilla的推荐配置)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS(HTTP Strict Transport Security),强制浏览器只用HTTPS访问
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 静态文件服务
location /static/ {
alias /app/staticfiles/; # 指向Django collectstatic后的目录
expires 1y;
add_header Cache-Control "public, immutable";
}
# 媒体文件服务(用户上传的图片等)
location /media/ {
alias /app/media/;
expires 1y;
}
# 将所有其他请求代理给Django后端
location / {
proxy_pass http://django_app;
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;
# 超时设置,防止长连接阻塞
proxy_connect_timeout 10s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
}
}
这个配置的精妙之处在于分层。
upstream
块定义了后端Django服务的地址,
server
块则定义了两个监听:一个纯HTTP的80端口,它唯一的任务就是把所有流量301重定向到HTTPS;另一个是真正的HTTPS服务,监听443端口。
location /static/
和
location /media/
是专门用来服务静态和媒体文件的,它们直接由Nginx读取磁盘文件并返回,完全不经过Django,这是性能的关键。
location /
则是一个兜底规则,把所有其他请求(比如
/api/users/
)都通过
proxy_pass
转发给
upstream
定义的Django服务。
proxy_set_header
系列指令至关重要,它们把原始的客户端IP、Host头等信息,以标准的HTTP头形式传递给Django,这样Django的
request.META
里才能拿到真实的用户IP,而不是Nginx容器的IP。
expires
和
Cache-Control
头,则告诉浏览器这些静态文件可以缓存一年,大大减少了重复请求。
4. 实操过程与核心环节实现
4.1 使用Docker Compose编排整个服务栈
Docker Compose是管理多容器应用的利器。它用一个
docker-compose.yml
文件,就定义了整个应用的“蓝图”,包括Django应用、PostgreSQL数据库、Nginx反向代理,甚至Redis缓存。这比手动
docker run
几十个参数要清晰、可靠得多。下面是一个完整的、可用于生产的
docker-compose.yml
示例:
# docker-compose.yml
version: '3.8'
services:
# Django应用服务
web:
build:
context: .
dockerfile: Dockerfile
image: my-django-app:latest
restart: unless-stopped
environment:
- DEBUG=False
- SECRET_KEY=${SECRET_KEY}
- DATABASE_URL=postgresql://postgres:postgres@db:5432/mydb
- ALLOWED_HOSTS=your-domain.com,www.your-domain.com
depends_on:
- db
volumes:
- static_volume:/app/staticfiles
- media_volume:/app/media
# PostgreSQL数据库服务
db:
image: postgres:15
restart: unless-stopped
environment:
- POSTGRES_DB=mydb
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
volumes:
- postgres_data:/var/lib/postgresql/data/
# Nginx反向代理服务
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- static_volume:/app/staticfiles
- media_volume:/app/media
- /etc/letsencrypt:/etc/letsencrypt
depends_on:
- web
# Certbot服务:用于申请和续期Let's Encrypt证书
certbot:
image: certbot/certbot
restart: on-failure
volumes:
- /etc/letsencrypt:/etc/letsencrypt
- /var/lib/letsencrypt:/var/lib/letsencrypt
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do sleep 12h & wait $${!}; certbot renew; done;'"
volumes:
postgres_data:
static_volume:
media_volume:
这个文件定义了四个服务。
web
服务从本地Dockerfile构建镜像,并通过
environment
注入了所有必要的环境变量。
db
服务启动一个PostgreSQL容器,数据通过
volumes
持久化到宿主机。
nginx
服务使用官方的
nginx:alpine
镜像,它体积小、启动快,并挂载了Nginx的配置文件和证书目录。最关键的
certbot
服务,它不是一个一次性任务,而是一个
永远在后台运行的守护进程
。它的
entrypoint
是一个无限循环:每12小时执行一次
certbot renew
命令。
certbot renew
会检查所有已签发的证书,如果有效期少于30天,就自动发起续期。由于
certbot
容器和
nginx
容器共享了
/etc/letsencrypt
卷,所以续期成功后,Nginx会立即使用新证书,整个过程对用户完全透明。
depends_on
确保了服务的启动顺序:数据库先启动,然后是Django应用,最后才是Nginx和Certbot。
volumes
定义了所有需要跨容器共享的数据卷,其中
static_volume
和
media_volume
是Django和Nginx之间共享静态和媒体文件的桥梁。
4.2 Let's Encrypt证书的自动化申请与续期
Let's Encrypt的自动化,是整个方案“免运维”的核心。手动申请证书的流程是:1) 在Nginx上配置一个临时的HTTP服务,监听80端口;2) 运行
certbot --standalone
,它会自己启动一个临时Web服务器,响应Let's Encrypt的验证请求;3) 验证通过后,证书被保存到
/etc/letsencrypt
。但这种方式在Docker环境下行不通,因为
--standalone
需要独占80端口,而我们的Nginx已经在监听了。因此,我们必须采用
--webroot
模式,让Certbot利用Nginx已经运行的Web服务来完成验证。
具体操作步骤如下:
-
首次申请
:在
docker-compose.yml中,先注释掉certbot服务,并确保nginx服务的ports部分只暴露80端口(即- "80:80"),暂时不暴露443。然后,启动web和nginx服务:docker-compose up -d web nginx。 -
准备验证目录
:进入Nginx容器,创建一个专门用于Let's Encrypt验证的目录:
docker-compose exec nginx mkdir -p /var/www/letsencrypt -
修改Nginx配置
:在
./nginx/conf.d/default.conf中,添加一个专门用于ACME验证的location块:
然后重启Nginx:server { listen 80; server_name your-domain.com www.your-domain.com; # ACME验证专用 location ^~ /.well-known/acme-challenge/ { root /var/www/letsencrypt; } # 其他所有请求,都301重定向到HTTPS(此时HTTPS还不存在,所以会失败,但没关系) location / { return 301 https://$server_name$request_uri; } }docker-compose exec nginx nginx -s reload。 -
运行Certbot
:在宿主机上,运行以下命令:
这个命令会启动一个临时的Certbot容器,它会向docker run -it --rm \ -v $(pwd)/nginx/conf.d:/etc/nginx/conf.d \ -v $(pwd)/nginx/nginx.conf:/etc/nginx/nginx.conf \ -v $(pwd)/nginx/letsencrypt:/etc/letsencrypt \ -v $(pwd)/nginx/www:/var/www/letsencrypt \ certbot/certbot certonly \ --webroot \ --webroot-path=/var/www/letsencrypt \ --email your-email@example.com \ --agree-tos \ --no-eff-email \ -d your-domain.com \ -d www.your-domain.com/.well-known/acme-challenge/路径下放置验证文件,然后通知Let's Encrypt服务器来访问这个URL。由于Nginx已经配置好了这个路径,验证必然成功。 -
启用HTTPS并启动Certbot守护进程
:验证成功后,取消
docker-compose.yml中certbot服务的注释,并将nginx的ports改为- "80:80"和- "443:443"。最后,执行docker-compose up -d,整个服务栈就全部启动了。Certbot守护进程会开始每12小时自动续期。
提示:
certbot renew命令本身是安全的,它只会续期即将过期的证书,不会影响其他证书。你可以随时手动运行docker-compose run --rm certbot certbot renew --dry-run来测试续期流程是否通畅,--dry-run参数表示这是一个模拟运行,不会真正修改证书。
4.3 Gunicorn作为WSGI服务器的深度配置
Django本身不是一个Web服务器,它只是一个遵循WSGI(Web Server Gateway Interface)规范的Python应用。要让它能被Nginx这样的Web服务器调用,必须有一个WSGI服务器作为中间件。
Gunicorn
(Green Unicorn)是Python社区最流行、最稳定的选择。它的工作原理是:启动一个主进程(Master Process),然后fork出多个工作进程(Worker Processes),每个工作进程都可以独立处理一个HTTP请求。这实现了真正的并发。
Gunicorn的配置,直接决定了Django应用的吞吐量和稳定性。一个生产环境的
gunicorn.conf.py
配置文件应该如下:
# gunicorn.conf.py
import multiprocessing
# 绑定地址和端口
bind = "0.0.0.0:8000"
bind_address = "0.0.0.0:8000"
port = "8000"
backlog = 2048
# 工作进程设置
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 100
# 超时设置
timeout = 30
keepalive = 5
graceful_timeout = 30
# 日志设置
accesslog = "-"
errorlog = "-"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
# 进程管理
daemon = False
pidfile = "/tmp/gunicorn.pid"
user = "appuser"
group = "appgroup"
umask = 0002
tmp_upload_dir = "/tmp"
# 安全设置
secure_scheme_headers = {'X-FORWARDED-PROTO': 'https'}
forwarded_allow_ips = '*'
这个配置的要点在于:
-
workers:工作进程数。公式cpu_count() * 2 + 1是一个经验法则。如果你的服务器有4个CPU核心,那么workers就是9。过多的进程会增加上下文切换开销,过少则无法充分利用CPU。 -
max_requests和max_requests_jitter:强制每个工作进程在处理1000个请求后自动重启。这是为了防止内存泄漏。jitter参数加入一个随机值(0-100),避免所有进程在同一时刻重启,造成服务抖动。 -
timeout和graceful_timeout:timeout是工作进程处理单个请求的最长等待时间,超过则被主进程杀死;graceful_timeout是主进程给工作进程优雅退出的时间,让它有机会处理完手头的请求。 -
accesslog = "-":将访问日志输出到stdout,这样Docker就能捕获并显示在docker logs里,方便集中日志管理。 -
user和group:确保Gunicorn以非root用户身份运行,与Dockerfile中的USER appuser保持一致。
在Dockerfile的
CMD
指令中,你应该使用这个配置文件:
CMD ["gunicorn", "--config", "gunicorn.conf.py", "myproject.wsgi:application"]
。这样,Gunicorn的所有行为都受控于这个精细的配置,而不是默认的、过于保守的参数。
5. 常见问题与排查技巧实录
5.1 “502 Bad Gateway”:Nginx与Django之间的“失联”
这是生产环境中最让人抓狂的错误之一。它意味着Nginx成功接收了用户的请求,但在尝试将请求转发给后端Django服务时失败了。原因通常有三个层面,需要按顺序排查。
第一层:网络连通性
。这是最基础的。进入Nginx容器,尝试
ping
或
telnet
Django服务:
docker-compose exec nginx ping web
docker-compose exec nginx telnet web 8000
如果
ping
不通,说明Docker网络配置有误,检查
docker-compose.yml
中
web
服务的
network_mode
是否被错误设置,或者
depends_on
是否遗漏。如果
telnet
不通,说明Django服务根本没有在8000端口监听。此时,进入
web
容器,检查Gunicorn进程是否在运行:
ps aux | grep gunicorn
。如果没看到,说明Dockerfile的
CMD
指令执行失败,或者
gunicorn.conf.py
里有语法错误,查看
docker logs <web_container_id>
获取详细错误。
第二层:Django应用健康状态
。即使Gunicorn进程在运行,Django应用本身也可能处于崩溃状态。最常见的原因是
ALLOWED_HOSTS
配置错误。当Nginx把
Host
头转发给Django时,如果这个域名不在
ALLOWED_HOSTS
列表里,Django会直接返回一个
DisallowedHost
异常,Gunicorn捕获到这个异常后,会返回一个500错误,而Nginx在收到500时,有时会将其转换为502。解决方案是,在
settings.py
中,确保
ALLOWED_HOSTS
包含了你的域名,并且在Docker Compose中,通过
environment
正确注入。
第三层:超时与资源限制
。如果Django应用处理一个请求需要很长时间(比如一个复杂的报表生成),而Nginx的
proxy_read_timeout
设置得太短(默认是60秒),Nginx就会在等待响应时超时,然后返回502。此时,你需要在Nginx的
location
块中,为这个特定的URL路径增加超时时间:
location /api/report/ {
proxy_pass http://django_app;
proxy_read_timeout 300; # 5分钟
}
同时,也要检查Gunicorn的
timeout
和
graceful_timeout
是否设置得足够长。
5.2 “404 Not Found” for Static Files:静态文件的“迷途”
当你看到CSS和JS文件返回404时,问题几乎100%出在Nginx的
location
配置和Django的
STATIC_ROOT
路径上。这是一个经典的“路径映射”错误。
首先,确认Django的
collectstatic
是否真的执行成功了。进入
web
容器,检查
/app/staticfiles/
目录是否存在,并且里面有文件:
docker-compose exec web ls -la /app/staticfiles/
如果这个目录是空的,说明
collectstatic
没有被执行,或者
STATIC_ROOT
路径在Dockerfile中被错误地覆盖了。
其次,检查Nginx的
location
配置。
alias
指令和
root
指令有本质区别。
alias /app/staticfiles/;
的意思是,当请求
/static/css/style.css
时,Nginx会去查找
/app/staticfiles/css/style.css
。而
root /app/staticfiles;
的意思是,它会去查找
/app/staticfiles/static/css/style.css
,这显然多了一层
static
目录。所以,
alias
是正确的选择。另外,
alias
路径末尾的斜杠
/
至关重要。
alias /app/staticfiles/;
是正确的,
alias /app/staticfiles;
(没有斜杠)则会导致路径拼接错误。
最后,检查Docker卷挂载。在
docker-compose.yml
中,
web
服务和
nginx
服务都必须挂载
static_volume
卷,并且挂载点必须一致。
web
服务挂载到
/app/staticfiles
,
nginx
服务也必须挂载到
/app/staticfiles
,这样才能保证Nginx读取的,就是Django写入的那个目录。
5.3 Certbot续期失败:证书的“断供危机”
certbot renew
失败,是运维中最令人焦虑的场景,因为它意味着你的网站HTTPS将在30天后失效。失败的原因通常有两类。
一类是网络验证失败
。最常见的错误是
urn:acme:error:connection
,意思是Let's Encrypt的服务器无法访问你的
/.well-known/acme-challenge/
路径。这通常是因为你的防火墙(如UFW、iptables)或云服务商的安全组(Security Group)没有放行80端口的入站流量。你需要确保服务器的80端口对外是开放的。另一个原因是DNS解析问题。
certbot renew
会检查你的域名是否解析到了当前服务器的IP。如果DNS记录是最近才修改的,可能存在全球缓存延迟,可以稍等一两个小时再试。
另一类是权限问题
。
certbot
容器需要读写
/etc/letsencrypt
目录。如果这个目录在宿主机上是由root用户创建的,而
certbot
容器是以非root用户运行的,就会出现权限拒绝。解决方案是,在宿主机上,将
/etc/letsencrypt
目录的所有者改为
1001
(即
certbot
容器内
certbot
用户的UID):
sudo chown -R 1001:1001 /etc/letsencrypt
或者,在
docker-compose.yml
中,为
certbot
服务添加`user: "
更多推荐
所有评论(0)