Django生产部署铁三角:Nginx+Gunicorn+MySQL容器化实践

发布时间:2026/10/15 15:27:59
Django生产部署铁三角:Nginx+Gunicorn+MySQL容器化实践
1. 为什么这套组合成了生产环境部署Django的“铁三角”你打开招聘网站搜Python后端岗十有八九要求里写着“熟悉Django生产部署流程”而括号里紧跟着的就是“NginxGunicornMySQL”。这不是凑数的堆砌而是经过十年以上高并发Web服务实战反复验证过的稳定结构。我最早在某高校实验室维护一个课程选课系统时用纯Python自带的runserver跑在云服务器上结果每到选课高峰就502——学生刷着刷着页面突然弹出“Bad Gateway”后台日志里全是Connection refused。后来换成Gunicorn配Nginx同一台2核4G的云服务器支撑起3000人同时抢课毫无压力。这背后不是玄学是每个组件各司其职、严丝合缝的工程逻辑。Django本身是个开发框架它的runserver命令只用于本地调试单进程、无超时控制、不处理静态文件、不防DDoS连HTTP/1.1的长连接都支持得磕磕绊绊。把它直接扔到公网等于把实验室的示波器拿到高压变电站去测电流。Gunicorn是真正的应用服务器WSGI Server它启动多个工作进程worker每个进程独立处理请求互不干扰能优雅重启、平滑升级、自动回收内存泄漏的进程还能通过配置限制每个worker的请求数量防止某个慢查询拖垮整个服务。Nginx则站在最外层像一个经验老道的门卫队长它负责接收所有HTTP请求把动态请求比如/api/user/转发给Gunicorn把静态资源CSS、JS、图片直接从磁盘返回不劳烦Python进程它还能做负载均衡、SSL终止、限流、缓存、防盗链——这些功能如果让Django自己干代码量翻三倍性能掉一半。MySQL则是数据的基石Django ORM再方便最终还是要靠它把数据落盘、加索引、做事务隔离。用Docker来打包这套组合不是为了赶时髦而是解决“在我机器上能跑换台服务器就报错”的经典困境。以前部署要手动装Python版本、编译依赖、配置Nginx虚拟主机、初始化MySQL数据库、设置用户权限……光是记笔记就得写满两页A4纸。现在一个docker-compose.yml文件把四个服务的镜像、端口、环境变量、挂载目录全写清楚docker-compose up -d一条命令整套环境5分钟内拉起来干净利落。更重要的是开发、测试、预发、生产四套环境配置差异只在几个环境变量里而不是四份面目全非的shell脚本。我带过的一个模拟项目X团队从3人扩到12人新成员入职第一天就能用docker-compose在本地复现和生产一模一样的环境省下的沟通成本够买半年咖啡了。这套方案的核心价值从来不是“炫技”而是确定性——确定它能在任何符合要求的Linux服务器上稳定运行确定它能扛住流量突增确定它升级时不会牵一发而动全身。接下来我们就一层层拆开这个“铁三角”看看每个螺丝怎么拧每个参数为什么这么设以及那些只有踩过坑的人才知道的细节。2. 整体架构设计与容器化思路拆解2.1 为什么必须分四个独立容器单容器不行吗这是新手最容易想当然的地方。看到“DjangoGunicornNginxMySQL”第一反应可能是“干脆全塞进一个Docker镜像里FROM ubuntu:22.04然后RUN apt install python3 nginx mysql-server最后CMD [sh, start.sh]——多简单” 我试过而且不止一次。第一次是在一个内部工具项目上图快结果上线三天后MySQL因为磁盘满了挂了整个容器重启Django和Nginx也跟着一起凉了用户看到的不是数据库错误而是整个网站502。第二次是想省事做热更新改了Nginx配置想docker exec -it myapp nginx -s reload结果发现容器里根本没装nginx因为启动脚本是直接python manage.py runserver硬编码的……问题根源在于违反了Docker的单一职责原则Single Responsibility Principle。一个容器应该只运行一个主进程这个进程崩溃容器就该退出这样Docker的健康检查和自动重启机制才能生效。MySQL是一个长期运行的守护进程Gunicorn是管理多个worker的主进程Nginx是反向代理和静态文件服务器Django代码只是Gunicorn加载的一个Python模块。把它们揉在一起等于让一个厨师既要炒菜、又要洗碗、还要管收银、还得修灶台——任何一个环节出问题整条流水线停摆。而分容器之后架构立刻清晰MySQL容器只负责数据存储。它暴露3306端口给内部网络外部完全不可见数据目录通过volume挂载到宿主机确保容器删了数据还在启动时自动执行初始化SQL脚本创建数据库和用户。DjangoGunicorn容器只负责业务逻辑。它不装Nginx不装MySQL客户端除了pymysql或mysqlclient这种Python驱动只装Python、Django、Gunicorn和项目依赖。它监听一个内部端口比如8000只接受来自Nginx的转发请求绝不暴露给公网。Nginx容器只负责流量调度。它不碰Python代码不连数据库只读取配置文件把/static/路径的请求指向Django容器的/app/staticfiles/目录把其他请求代理到http://django:8000注意这里是容器名django不是localhost。可选Redis容器虽然标题没提但实际项目几乎必加用于Django的缓存、Session后端、Celery任务队列。它和MySQL一样是纯粹的基础设施服务理应独立。这种设计带来的好处是立竿见影的故障隔离MySQL慢查询卡死只影响Django容器的数据库连接池Nginx依然能返回静态页面和503错误页弹性伸缩流量大了可以只增加DjangoGunicorn容器的数量docker-compose scale django4MySQL和Nginx保持1个即可独立升级Nginx要升级到1.25只需改docker-compose.yml里的镜像标签docker-compose up -d nginxDjango和MySQL完全不受影响资源可控可以给MySQL容器分配更多内存mem_limit: 2g给Nginx分配更少CPUcpus: 0.5精细化控制。提示docker-compose.yml中服务间的通信靠的是Docker内置的DNS服务。当你定义了services: { django: ..., nginx: ..., mysql: ... }Docker会自动为每个服务创建一个同名的DNS记录。所以在Nginx配置里写proxy_pass http://django:8000;Nginx容器启动时会自动解析django为Django容器的IP地址。这比写死172.18.0.3可靠一万倍因为IP是Docker动态分配的。2.2 镜像选择官方镜像 vs 自定义基础镜像选什么基础镜像直接决定了后续维护的难易程度。网上很多教程用FROM python:3.11-slim看起来很轻量但“slim”版里缺了gcc、musl-dev等编译工具而Django项目里常见的psycopg2PostgreSQL驱动、mysqlclientMySQL驱动都需要编译C扩展。结果就是pip install时疯狂报错error: command gcc failed逼着你在Dockerfile里RUN apt-get update apt-get install -y build-essential又把镜像体积撑大了还引入了不必要的安全风险。我的经验是生产环境无脑选python:3.11-slim-bookwormDebian 12。Bookworm是Debian最新的稳定版软件包更新及时安全补丁完善。slim版已经包含了gcc、make等基础构建工具足够编译绝大多数Python C扩展。它比python:3.11基于Debian 11少了近100MB比python:3.11-bullseye旧版Debian更安全。至于Nginx和MySQL直接用官方镜像nginx:1.25-alpineAlpine体积小适合边缘部署或nginx:1.25基于Debian兼容性更好mysql:8.0MySQL 8.0是当前最稳定的LTS版本JSON字段、窗口函数、角色管理都成熟了。自定义基础镜像比如自己FROM ubuntu:22.04只在一种情况下值得考虑你的公司有严格的合规要求必须使用内部镜像仓库且所有镜像都要经过安全扫描和签名。否则重复造轮子纯属给自己找麻烦。官方镜像由对应项目的维护者亲自构建和测试每周都有安全更新你花一周时间写的Dockerfile可能还不如人家一行FROM python:3.11-slim-bookworm来得稳。2.3 网络与存储bridge网络与命名卷是默认最优解Docker默认的bridge网络对这个场景来说就是“开箱即用”的最优解。它为docker-compose项目创建一个独立的内部网络所有服务都在这个网络里通过服务名互相发现。你不需要手动docker network createdocker-compose up会自动搞定。关键是要理解bridge网络的两个边界内部边界Django容器能通过mysql这个域名访问MySQL容器的3306端口Nginx能通过django访问Django容器的8000端口。这个通信走的是Docker的虚拟网桥速度极快延迟微乎其微。外部边界只有Nginx容器需要把80HTTP和443HTTPS端口映射到宿主机ports: [80:80, 443:443]其他容器的端口如MySQL的3306、Django的8000绝对不要映射映射出去等于把数据库密码、API密钥直接暴露在公网上是严重安全隐患。存储方面必须用命名卷named volume而不是绑定挂载bind mount。volumes: [./data:/var/lib/mysql]这种写法看着直观但问题很大./data是相对路径docker-compose.yml放在哪./data就指向哪跨机器迁移时路径错乱更重要的是它把宿主机的文件系统权限直接暴露给了容器MySQL容器以mysql用户身份写入./data而宿主机上这个目录可能是root所有导致权限冲突容器启动失败。命名卷是Docker管理的抽象存储单元volumes: mysql_data: static_files:然后在MySQL服务里引用services: mysql: image: mysql:8.0 volumes: - mysql_data:/var/lib/mysqlDocker会在宿主机的/var/lib/docker/volumes/下创建一个独立目录由Docker统一管理权限和生命周期。你docker volume rm mysql_data数据就彻底删除docker volume ls能清晰看到所有卷。对于Django的静态文件同样用static_files卷让Nginx容器和Django容器都能挂载它Django收集完静态文件python manage.py collectstatic后Nginx就能直接从这个卷里读取不用再搞复杂的文件同步脚本。3. 核心组件配置与实操要点详解3.1 Django容器从开发模式到生产模式的彻底转变Django项目从本地runserver切换到生产Gunicorn绝不是改个命令那么简单而是涉及配置、安全、性能的全面重构。核心在于理解Django的settings.py如何根据环境动态加载。首先环境变量驱动配置是唯一正道。在settings.py顶部加入import os from pathlib import Path # Build paths inside the project like this: BASE_DIR / subdir. BASE_DIR Path(__file__).resolve().parent.parent.parent # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY os.environ.get(DJANGO_SECRET_KEY, dev-key-change-in-prod) # SECURITY WARNING: dont run with debug turned on in production! DEBUG os.environ.get(DJANGO_DEBUG, False) True # Allowed hosts - critical for security! ALLOWED_HOSTS os.environ.get(DJANGO_ALLOWED_HOSTS, localhost,127.0.0.1).split(,)这样docker-compose.yml里就可以通过environment注入不同值services: django: build: . environment: - DJANGO_SECRET_KEYyour-real-secret-key-here - DJANGO_DEBUGFalse - DJANGO_ALLOWED_HOSTSmyapp.com,www.myapp.comALLOWED_HOSTS是重中之重。DEBUGFalse时Django会严格校验HTTP请求头里的Host字段。如果你没设或者只设了[*]所有请求都会返回400 Bad Request。线上必须填你的真实域名多个用逗号隔开。其次静态文件处理必须交给Nginx。Django的STATIC_URL和STATIC_ROOT要明确区分# Static files (CSS, JavaScript, Images) STATIC_URL /static/ STATIC_ROOT BASE_DIR / staticfiles # 这是collectstatic输出的目录 # Media files (user uploads) MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / mediaSTATIC_ROOT必须是一个绝对路径指向一个空目录。Django容器启动时第一件事就是执行python manage.py collectstatic --noinput把所有App里的static/文件和第三方包的静态文件全部复制到STATIC_ROOT即/app/staticfiles。然后Nginx配置里就能直接把这个目录映射为/static/路径。第三数据库配置要完全脱离硬编码DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: os.environ.get(MYSQL_DATABASE, myapp), USER: os.environ.get(MYSQL_USER, root), PASSWORD: os.environ.get(MYSQL_PASSWORD, ), HOST: os.environ.get(MYSQL_HOST, mysql), # 注意这里是mysql不是localhost PORT: os.environ.get(MYSQL_PORT, 3306), OPTIONS: { init_command: SET sql_modeSTRICT_TRANS_TABLES, charset: utf8mb4, }, } }HOST填mysql是因为Docker Compose的DNS服务会把mysql解析成MySQL容器的IP。charset设为utf8mb4是为了完整支持emojisql_mode设为严格模式避免MySQL静默截断数据。最后Gunicorn配置单独写一个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 preload True # 日志 accesslog /var/log/gunicorn/access.log errorlog /var/log/gunicorn/error.log loglevel info access_log_format %(h)s %(l)s %(u)s %(t)s %(r)s %(s)s %(b)s %(f)s %(a)s关键参数解释workers: 工作进程数。公式cpu_count * 2 1是Gunicorn官方推荐的起始值。2核服务器就开5个worker既能充分利用CPU又不会因进程过多导致上下文切换开销过大。max_requests: 每个worker处理1000个请求后自动重启。这是为了防止Python内存泄漏累积导致worker越来越慢甚至OOM。preload: 在fork worker之前先加载一次Django应用。这样所有worker共享同一个已加载的应用实例节省内存启动更快。accesslog和errorlog: 日志必须写到文件不能只打到stdout。Docker会捕获stdout但生产环境的日志分析需要结构化文件。注意Django容器的Dockerfile里CMD指令应该是[gunicorn, --config, gunicorn.conf.py, myproject.wsgi:application]其中myproject.wsgi:application要替换成你项目真实的WSGI模块路径通常是your_project_name.wsgi:application。3.2 MySQL容器不只是装个数据库而是构建一个可靠的数据中心MySQL容器的配置90%的精力应该花在初始化脚本和安全加固上而不是纠结于my.cnf的调优参数。云服务器上的MySQL首要目标是“不挂”其次才是“快”。初始化脚本init.sql放在docker-compose.yml同级目录-- 创建数据库 CREATE DATABASE IF NOT EXISTS myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 创建专用用户禁止远程root登录 CREATE USER myapp_user% IDENTIFIED BY strong_password_here; GRANT ALL PRIVILEGES ON myapp.* TO myapp_user%; -- 刷新权限 FLUSH PRIVILEGES;然后在docker-compose.yml里告诉MySQL容器执行它services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root_password_here MYSQL_DATABASE: myapp MYSQL_USER: myapp_user MYSQL_PASSWORD: strong_password_here volumes: - mysql_data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro/docker-entrypoint-initdb.d/是MySQL官方镜像的魔法目录。容器第一次启动、检测到/var/lib/mysql为空时会自动执行这个目录下所有.sql和.sh文件。这样数据库、用户、权限、初始数据全在容器启动时一步到位无需人工docker exec进去操作。安全加固的关键点禁用root远程登录MySQL 8.0默认root用户只允许从localhost登录。我们的Django容器是另一个网络节点所以必须创建一个专用用户如myapp_user并明确授权myapp_user%%代表任意主机。永远不要用root账号连生产数据库。强制SSL连接可选但推荐在docker-compose.yml的MySQL环境变量里加MYSQL_SSL1然后在Django的DATABASES配置里加上OPTIONS: { ssl: { ca: /etc/ssl/certs/ca-certificates.crt, # Alpine系统路径 } }这样Django和MySQL之间的所有通信都是加密的即使网络被嗅探密码和数据也不会泄露。定期备份Docker本身不提供备份。你需要一个单独的备份容器或者用宿主机的cron定时执行docker exec mysql mysqldump -u myapp_user -pstrong_password_here myapp /backup/myapp_$(date %F).sql。备份文件必须存到宿主机的独立目录不能存在MySQL容器的volume里——万一volume损坏备份也跟着没了。实操心得MySQL容器第一次启动会比较慢可能1-2分钟因为它在初始化数据目录、生成SSL证书、执行初始化脚本。别慌看docker logs mysql等出现[System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections.就说明好了。如果卡在[Entrypoint] Starting MySQL 8.0大概率是init.sql语法错误检查SQL文件。3.3 Nginx容器反向代理的艺术与静态文件的终极归宿Nginx的配置文件nginx.conf是整个架构的“交通指挥中心”。它不复杂但每一行都关乎性能和安全。一个生产可用的最小nginx.conf# 全局配置 user nginx; worker_processes auto; pid /var/run/nginx.pid; events { worker_connections 1024; } 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 $http_referer $http_user_agent $http_x_forwarded_for; access_log /var/log/nginx/access.log main; error_log /var/log/nginx/error.log warn; # 性能优化 sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; # Gzip压缩减小传输体积 gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xmlrss text/javascript; # 包含所有站点配置 include /etc/nginx/conf.d/*.conf; }核心是/etc/nginx/conf.d/default.conf这才是业务逻辑upstream django_app { server django:8000; } server { listen 80; server_name myapp.com www.myapp.com; # 强制HTTPS重定向 return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name myapp.com www.myapp.com; # SSL证书需提前准备好 ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; # SSL安全配置推荐Mozilla的Intermediate配置 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:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; # 静态文件直接由Nginx服务 location /static/ { alias /app/staticfiles/; expires 1y; add_header Cache-Control public, immutable; } location /media/ { alias /app/media/; expires 7d; } # 动态请求全部转发给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_redirect off; proxy_buffering on; client_max_body_size 100M; } }逐条解析upstream django_app: 定义了一个上游服务器组名字叫django_app里面只有一个服务器django:8000。这样proxy_pass就可以用这个名字方便以后加负载均衡。return 301 https://...: HTTP请求一律301重定向到HTTPS。这是现代Web的底线没有商量余地。location /static/: 这是Nginx的“特权”。它直接从/app/staticfiles/这个目录也就是Django容器挂载的static_files卷读取文件不经过任何Python代码。expires 1y和Cache-Control让浏览器缓存一年极大减轻后端压力。proxy_set_header: 这几行是灵魂。Django需要知道原始请求是HTTP还是HTTPSX-Forwarded-Proto真实的客户端IP是什么X-Real-IP和X-Forwarded-For否则request.is_secure()永远返回Falserequest.META.get(REMOTE_ADDR)永远是Nginx的IP。没有这几行你的网站登录、CSRF保护、地理位置判断全都会错乱。client_max_body_size 100M: 允许用户上传最大100MB的文件。Django默认只允许2.5MB这个值必须和Django的DATA_UPLOAD_MAX_MEMORY_SIZE、FILE_UPLOAD_MAX_MEMORY_SIZE保持一致。SSL证书的获取推荐用certbotLets Encrypt官方客户端。在宿主机上运行# 安装certbot sudo apt install certbot python3-certbot-nginx # 获取证书需要先将域名DNS解析到这台云服务器IP sudo certbot --nginx -d myapp.com -d www.myapp.comcertbot会自动修改Nginx配置添加SSL相关指令并把证书放在/etc/letsencrypt/live/myapp.com/。然后在docker-compose.yml里把整个目录挂载进Nginx容器services: nginx: image: nginx:1.25 volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./default.conf:/etc/nginx/conf.d/default.conf:ro - /etc/letsencrypt:/etc/nginx/ssl:ro # 关键把宿主机证书挂进来 - static_files:/app/staticfiles - media_files:/app/media4. Docker Compose全流程实现与关键步骤实录4.1 项目目录结构清晰胜于一切一个健壮的Docker项目目录结构本身就是一份文档。我坚持的结构如下所有路径都相对于项目根目录my-django-project/ ├── docker-compose.yml # 主编排文件 ├── Dockerfile # DjangoGunicorn的构建文件 ├── nginx/ │ ├── nginx.conf # Nginx全局配置 │ └── conf.d/ │ └── default.conf # 站点配置 ├── mysql/ │ └── init.sql # 数据库初始化脚本 ├── app/ # Django项目源码可git submodule或直接放这里 │ ├── manage.py │ ├── myproject/ # 项目包 │ │ ├── __init__.py │ │ ├── settings/ │ │ │ ├── __init__.py │ │ │ ├── base.py # 公共配置 │ │ │ ├── local.py # 本地开发配置 │ │ │ └── production.py # 生产配置被base.py导入 │ │ ├── wsgi.py │ │ └── asgi.py │ └── requirements.txt ├── .env # 环境变量文件git ignore └── README.md这个结构的好处是docker-compose.yml非常干净所有配置都外置app/目录可以独立作为Git仓库和Docker部署解耦nginx/和mysql/目录集中管理所有基础设施配置新人一眼就能看清整个技术栈。4.2 docker-compose.yml从零开始编写这是整个部署的“总开关”必须精确无误。以下是完整的、可直接运行的docker-compose.yml已按最佳实践配置version: 3.8 # 定义网络显式声明便于理解 networks: backend: driver: bridge # 定义命名卷集中管理 volumes: mysql_data: static_files: media_files: services: # MySQL服务 mysql: image: mysql:8.0 container_name: myapp-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_PASSWORD: ${MYSQL_PASSWORD} volumes: - mysql_data:/var/lib/mysql - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro networks: - backend healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -u, root, -p${MYSQL_ROOT_PASSWORD}] timeout: 20s retries: 10 start_period: 40s # DjangoGunicorn服务 django: build: . container_name: myapp-django restart: unless-stopped environment: - DJANGO_SECRET_KEY${DJANGO_SECRET_KEY} - DJANGO_DEBUGFalse - DJANGO_ALLOWED_HOSTS${DJANGO_ALLOWED_HOSTS} - MYSQL_HOSTmysql - MYSQL_PORT3306 - MYSQL_DATABASE${MYSQL_DATABASE} - MYSQL_USER${MYSQL_USER} - MYSQL_PASSWORD${MYSQL_PASSWORD} volumes: - ./app:/app - static_files:/app/staticfiles - media_files:/app/media depends_on: mysql: condition: service_healthy networks: - backend healthcheck: test: [CMD, curl, -f, http://localhost:8000/health/] # 需要在Django里写一个/health/视图 timeout: 20s retries: 10 start_period: 60s # Nginx服务 nginx: image: nginx:1.25 container_name: myapp-nginx restart: unless-stopped ports: - 80:80 - 443:443 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro - /etc/letsencrypt:/etc/nginx/ssl:ro - static_files:/app/staticfiles - media_files:/app/media depends_on: - django networks: - backend healthcheck: test: [CMD, curl, -f, http://localhost] timeout: 10s retries: 5 start_period: 30s关键点说明restart: unless-stopped: 容器意外退出后自动重启但docker stop命令可以正常停止它符合运维习惯。healthcheck: 为每个服务定义健康检查。MySQL用mysqladmin pingDjango用curl http://localhost:8000/health/你需要在Django里写一个简单的视图返回200Nginx用curl http://localhost。depends_on的condition: service_healthy确保Django只在MySQL健康后才启动避免Django启动时连不上数据库而崩溃。volumes挂载./app:/app是绑定挂载方便开发时代码实时生效static_files和media_files是命名卷用于生产环境的持久化。environment中的${VAR}变量从.env文件读取。.env文件内容示例MYSQL_ROOT_PASSWORDsuper-secure-root-pass MYSQL_DATABASEmyapp MYSQL_USERmyapp_user MYSQL_PASSWORDmyapp-user-pass DJANGO_SECRET_KEYdjango-insecure-very-long-random-string-here DJANGO_ALLOWED_HOSTSmyapp.com,www.myapp.com4.3 Dockerfile精简、安全、可复现Django服务的Dockerfile目标是构建一个最小、最快、最安全的镜像# 使用官方Python slim镜像 FROM python:3.11-slim-bookworm # 设置工作目录 WORKDIR /app # 复制requirements.txt并安装系统依赖Debian COPY app/requirements.txt . RUN apt-get update apt-get install -y \ gcc \ default-libmysqlclient-dev \ libpq-dev \ curl \ rm -rf /var/lib/apt/lists/* # 安装Python依赖 RUN pip install --no-cache-dir -r requirements.txt # 复制整个Django项目 COPY app/ . # 创建非root用户安全最佳实践 RUN useradd -m -u 1001 -G root -d /home/appuser appuser USER appuser # 收集静态文件在非root用户下执行 RUN python manage.py collectstatic --noinput # 暴露端口 EXPOSE 8000 # 启动命令 CMD [gunicorn, --config, gunicorn.conf.py, myproject.wsgi:application]这个Dockerfile的亮点分层缓存先复制requirements.txt再pip install这样只要requirements.txt没变pip install这层就不会重新构建极大加快CI/CD速度。非root用户最后一行USER appuser让Gunicorn以普通用户身份运行。即使Gunicorn被攻破攻击者也无法获得root权限只能在/app目录下活动。静态文件预收集collectstatic在构建镜像时就执行完毕容器启动时无需等待秒级就绪。4.4 一键部署从零到上线的完整命令流所有准备工作就绪后部署就是三步准备环境在云服务器上安装Docker和Docker Compose。# Ubuntu 22.04 sudo apt update sudo apt install -y curl gnupg2 software-properties-common curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -