Django部署最佳实践 - 生产环境部署与运维

📂 所属阶段:第三部分 — 高级主题
🎯 难度等级:高级
⏰ 预计学习时间:6-8小时
🎒 前置知识:性能优化, 安全最佳实践


目录


部署核心架构与原则

极简生产架构

我们不需要一开始就上AWS ELB这种重型组件,先从单机Docker可扩展架构切入:

Internet

Nginx (反向代理、SSL、静态文件)

Gunicorn Workers (WSGI服务)

PostgreSQL (主数据库)

Redis (缓存、异步队列broker)

Celery (可选:异步任务)

5条铁则

必记原则
  1. 安全第一:DEBUG必须False、最小权限、强制HTTPS
  2. 高可用雏形:多Gunicorn Worker、进程自动重启
  3. 可快速扩容:预留Docker Swarm/K8s接口
  4. 基础监控要到位:日志、进程存活、资源使用率
  5. 自动化半自动化结合:先能用Shell脚本,再上CI/CD :::

轻量级环境准备

这里只列通用Linux服务器必须的环境,不需要的依赖(比如HTOP可以自己选)已经砍掉:

:::code-group

#!/bin/bash
sudo apt update && sudo apt upgrade -y

# 安装生产核心依赖
sudo apt install -y \
    python3-pip python3-venv python3-dev \
    build-essential libpq-dev libssl-dev \
    nginx supervisor git curl

# 创建非root应用用户(安全必做)
sudo useradd --system --home /app --shell /bin/bash app
sudo mkdir -p /app /var/log/myapp /var/run/myapp
sudo chown -R app:app /app /var/log/myapp /var/run/myapp

# 防火墙配置
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
# .env 放在项目根目录,不要提交到Git!
DEBUG=False
SECRET_KEY=your-very-long-random-secret-key-here
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com

# 数据库
DB_NAME=myapp
DB_USER=myappuser
DB_PASSWORD=your-strong-db-password
DB_HOST=db
DB_PORT=5432
DB_CONN_MAX_AGE=60

# Redis
REDIS_URL=redis://redis:6379/0

Docker Compose全栈部署

这是最快能跑的单机生产级方案,以后可以直接升级到Swarm/K8s:

:::code-group

FROM python:3.11-slim

# 提前设置环境变量减少构建层
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DJANGO_SETTINGS_MODULE=myproject.settings.production

# 安装系统依赖(合并成一层)
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc libpq-dev libjpeg-dev zlib1g-dev \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# 复制依赖(缓存层,只在requirements.txt变化时重建)
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
    pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY . .

# 切换非root用户
RUN useradd --create-home --shell /bin/bash app && \
    chown -R app:app /app
USER app

# 收集静态文件(这里是生产模式,提前执行)
RUN python manage.py collectstatic --noinput

EXPOSE 8000
CMD ["gunicorn", "--config", "gunicorn.conf.py", "myproject.wsgi:application"]
Django>=4.2,<5.0
gunicorn==21.2.0
psycopg2-binary==2.9.7
redis==5.0.1
celery==5.3.4
django-redis==5.4.0
Pillow==10.0.1
python-decouple==3.8
whitenoise==6.6.0
version: '3.8'

services:
  db:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
      - ./postgres/init:/docker-entrypoint-initdb.d/ # 可选:初始化脚本
    environment:
      - POSTGRES_DB=${DB_NAME}
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    networks:
      - app-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-}
    volumes:
      - redis_data:/data
    networks:
      - app-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  web:
    build: .
    ports:
      - "127.0.0.1:8000:8000" # 只暴露给本地Nginx
    environment:
      - DB_HOST=db
      - REDIS_URL=${REDIS_URL}
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - static_volume:/app/static
      - media_volume:/app/media
    networks:
      - app-network
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/conf.d:/etc/nginx/conf.d
      - static_volume:/app/static:ro
      - media_volume:/app/media:ro
      - ./ssl:/etc/ssl:ro
    depends_on:
      - web
    networks:
      - app-network
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
  static_volume:
  media_volume:

networks:
  app-network:
    driver: bridge

:::


基础但高效的Nginx配置

# nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    use epoll;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # 日志格式带时间戳
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$request_time"';
    
    access_log /var/log/nginx/access.log main;
    
    # 基础性能优化
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    client_max_body_size 16M;
    
    # Gzip压缩静态资源
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/javascript application/json application/javascript image/svg+xml;
    
    include /etc/nginx/conf.d/*.conf;
}
# nginx/conf.d/myapp.conf
upstream django {
    server web:8000;
}

# 强制HTTPS重定向
server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;
    
    # SSL证书(建议用Certbot免费申请)
    ssl_certificate /etc/ssl/certs/fullchain.pem;
    ssl_certificate_key /etc/ssl/private/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:10m;
    
    # 安全头(必加)
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    
    # 静态文件(只读)
    location /static/ {
        alias /app/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
    
    # 媒体文件(禁止执行脚本)
    location /media/ {
        alias /app/media/;
        expires 30d;
        add_header Cache-Control "public";
        location ~* \.(php|py|sh)$ { deny all; return 404; }
    }
    
    # 代理到Django
    location / {
        proxy_pass http://django;
        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_read_timeout 60s;
        proxy_buffering on;
    }
    
    # 健康检查端点
    location /health/ {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }
}

Gunicorn配置与优化

# gunicorn.conf.py
import multiprocessing

# 核心配置
bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 + 1  # 通用公式,CPU密集型可减少
worker_class = "sync"  # 普通应用用sync足够,I/O密集型用gevent
worker_connections = 1000
timeout = 30
max_requests = 1000  # 防止内存泄漏,随机重启
max_requests_jitter = 100

# 性能优化
preload_app = True  # 预加载Django,减少Worker启动内存
tmp_upload_dir = "/dev/shm"  # 用内存临时目录

PostgreSQL生产安全与备份

快速备份脚本(可选Celery定时)

# myapp/management/commands/backup_db.py
import os
import datetime
import subprocess
from django.conf import settings
from django.core.management.base import BaseCommand

class Command(BaseCommand):
    help = "备份PostgreSQL数据库"

    def handle(self, *args, **options):
        timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
        backup_dir = '/backups'
        os.makedirs(backup_dir, exist_ok=True)
        backup_file = f"{backup_dir}/myapp_{timestamp}.sql.gz"

        try:
            # 带压缩的备份命令
            cmd = [
                'pg_dump',
                '-h', settings.DATABASES['default']['HOST'],
                '-U', settings.DATABASES['default']['USER'],
                '-d', settings.DATABASES['default']['NAME'],
            ]
            env = os.environ.copy()
            env['PGPASSWORD'] = settings.DATABASES['default']['PASSWORD']

            with open(backup_file, 'wb') as f:
                proc1 = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE)
                proc2 = subprocess.Popen(['gzip'], stdin=proc1.stdout, stdout=f)
                proc1.stdout.close()
                proc2.wait()

            # 清理7天前的备份
            cutoff = datetime.datetime.now() - datetime.timedelta(days=7)
            for filename in os.listdir(backup_dir):
                file_path = os.path.join(backup_dir, filename)
                if os.path.getmtime(file_path) < cutoff.timestamp():
                    os.remove(file_path)

            self.stdout.write(self.style.SUCCESS(f"备份成功: {backup_file}"))
        except Exception as e:
            self.stderr.write(self.style.ERROR(f"备份失败: {str(e)}"))

本章小结

今天我们完成了Django单机生产级部署的闭环

  1. 先搞懂了极简架构和5条铁则
  2. 配置了轻量级的服务器和Python环境
  3. 用Docker Compose搭好了全栈环境(PostgreSQL、Redis、Nginx、Gunicorn)
  4. 写了基础但安全高效的Nginx和Gunicorn配置
  5. 加了PostgreSQL的定时备份脚本
下一步

生产环境上线前,一定要去申请免费的Certbot SSL证书,测试所有功能,配置Supervisor(如果不用Docker的重启策略),再加个简单的监控比如Prometheus+Grafana!