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服务来完成验证。

具体操作步骤如下:

  1. 首次申请 :在 docker-compose.yml 中,先注释掉 certbot 服务,并确保 nginx 服务的 ports 部分只暴露80端口(即 - "80:80" ),暂时不暴露443。然后,启动 web nginx 服务: docker-compose up -d web nginx
  2. 准备验证目录 :进入Nginx容器,创建一个专门用于Let's Encrypt验证的目录:
    docker-compose exec nginx mkdir -p /var/www/letsencrypt
    
  3. 修改Nginx配置 :在 ./nginx/conf.d/default.conf 中,添加一个专门用于ACME验证的 location 块:
    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;
        }
    }
    
    然后重启Nginx: docker-compose exec nginx nginx -s reload
  4. 运行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
    
    这个命令会启动一个临时的Certbot容器,它会向 /.well-known/acme-challenge/ 路径下放置验证文件,然后通知Let's Encrypt服务器来访问这个URL。由于Nginx已经配置好了这个路径,验证必然成功。
  5. 启用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: "

更多推荐