Docker 化与 Compose 编排
学习目标
构建应用镜像,组合 MySQL、Gunicorn、Nginx,区分镜像、容器和数据卷,完成一次可重复的本机生产形态部署。
为什么应用容器不应该保存唯一的数据副本
镜像保存代码和依赖,容器运行进程,卷保存数据库与媒体。重建镜像可以替换代码,但不应删除数据库和用户图片。Compose 将服务网络和启动依赖写成配置;数据库先健康,再运行迁移和静态收集,最后启动 Web。
Dockerfile 的构建阶段安装编译工具生成 wheel,运行阶段只留下必要库和依赖。应用使用普通 uid 10001;一次性 init 服务仅负责卷目录归属。数据库不映射宿主机端口,Web 只供内部 Nginx 访问,对外先绑定 127.0.0.1:8010,避免和本站 8001 冲突。
具体怎么做
安装并启动支持 Compose V2 的 Docker,执行 docker version 和 docker compose version。进入练习根目录,保存下面文件,然后:
cp .env.example .env
python -c "import secrets; print(secrets.token_urlsafe(48))"
把生成结果填入 .env 的 DJANGO_SECRET_KEY;再分别生成数据库应用密码和 root 密码,不复用。密钥至少 50 字符;Linux/macOS 执行 chmod 600 .env。这些值只保存在自己机器中,不提交或贴到讨论区。
docker compose config --quiet
docker compose build web
docker compose up -d --wait db
docker compose run --rm init
docker compose run --rm web python manage.py migrate --noinput
docker compose run --rm web python manage.py collectstatic --noinput
docker compose run --rm web python manage.py createsuperuser
docker compose up -d web nginx
docker compose ps
构建镜像需要下载基础镜像和依赖,请保证 Docker 能访问相应源。这里不自动导入本站 SQL、复制本站配置或迁移本站数据。若迁移自己的练习数据,用上一章的方案明确执行。
本章新增或替换的完整文件
以下路径均相对于练习项目根目录。替换同名文件的全部内容;未列出的文件保持上一章版本。
.dockerignore
.git
.venv
.env
.env.*
*.sqlite3
media
staticfiles
backups
__pycache__
**/__pycache__
*.pyc
certs
.env.example
# 仅供练习项目使用;复制为 .env 后填写自己的随机密码和密钥。
DJANGO_SECRET_KEY=replace-with-a-random-secret-at-least-50-characters
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
MYSQL_DATABASE=zhixing
MYSQL_USER=zhixing
MYSQL_PASSWORD=replace-with-a-random-database-password
MYSQL_ROOT_PASSWORD=replace-with-a-different-root-password
HTTP_PORT=8010
EMAIL_HOST=
EMAIL_PORT=587
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
DEFAULT_FROM_EMAIL=noreply@example.invalid
TLS_CERT_DIR=./certs
Dockerfile
FROM python:3.14-slim-bookworm AS builder
WORKDIR /build
RUN apt-get update && apt-get install -y --no-install-recommends build-essential pkg-config default-libmysqlclient-dev && rm -rf /var/lib/apt/lists/*
COPY requirements*.txt ./
RUN pip wheel --wheel-dir /wheels -r requirements-prod.txt
FROM python:3.14-slim-bookworm
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends libmariadb3 && rm -rf /var/lib/apt/lists/* && groupadd --gid 10001 app && useradd --uid 10001 --gid 10001 --no-create-home app
COPY --from=builder /wheels /wheels
COPY requirements*.txt ./
RUN pip install --no-index --find-links /wheels -r requirements-prod.txt && rm -rf /wheels
COPY --chown=10001:10001 . .
RUN mkdir -p media staticfiles && chown app:app media staticfiles
USER 10001:10001
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "2", "--threads", "2", "--timeout", "60", "--access-logfile", "-"]
compose.yaml
name: zhixing-tutorial
services:
db:
image: mysql:8.4
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE:?请设置 MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER:?请设置 MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?请设置 MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?请设置 MYSQL_ROOT_PASSWORD}
command: ["--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"]
volumes: ["database:/var/lib/mysql"]
healthcheck:
test: ["CMD-SHELL", "MYSQL_PWD=$$MYSQL_PASSWORD mysql -h127.0.0.1 -u$$MYSQL_USER $$MYSQL_DATABASE -e 'SELECT 1' >/dev/null 2>&1"]
interval: 5s
timeout: 5s
retries: 40
restart: unless-stopped
init:
image: zhixing-blog:6.1
user: "0:0"
entrypoint: ["sh", "-c", "chown 10001:10001 /app/media /app/staticfiles"]
volumes: ["media:/app/media", "static:/app/staticfiles"]
web:
image: zhixing-blog:6.1
build: .
environment:
DJANGO_SETTINGS_MODULE: config.production
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:?请设置 DJANGO_SECRET_KEY}
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1}
DB_ENGINE: mysql
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_HOST: db
EMAIL_HOST: ${EMAIL_HOST:-}
EMAIL_PORT: ${EMAIL_PORT:-587}
EMAIL_HOST_USER: ${EMAIL_HOST_USER:-}
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD:-}
DEFAULT_FROM_EMAIL: ${DEFAULT_FROM_EMAIL:-noreply@example.invalid}
volumes: ["media:/app/media", "static:/app/staticfiles"]
depends_on:
db: {condition: service_healthy}
init: {condition: service_completed_successfully}
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/', timeout=5)"]
interval: 15s
timeout: 8s
retries: 10
restart: unless-stopped
nginx:
image: nginx:stable-alpine
ports: ["127.0.0.1:${HTTP_PORT:-8010}:80"]
volumes:
- ./deploy/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- static:/app/staticfiles:ro
- media:/app/media:ro
depends_on:
web: {condition: service_healthy}
restart: unless-stopped
volumes:
database:
media:
static:
config/production.py
import os
os.environ["DJANGO_DEBUG"] = "0"
from .settings import *
if os.environ.get("DB_ENGINE") != "mysql":
raise ImproperlyConfigured("本教程的生产配置要求独立 MySQL 数据库")
DEBUG = False
deploy/nginx.conf
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;
server {
listen 80;
server_name localhost;
client_max_body_size 3m;
add_header X-Content-Type-Options nosniff always;
location /static/ { alias /app/staticfiles/; expires 7d; }
location /media/ {
alias /app/media/;
add_header X-Content-Type-Options nosniff always;
add_header Content-Security-Policy "default-src 'none'; sandbox" always;
}
location ~ /\. { deny all; }
location /accounts/ {
limit_req zone=auth burst=10 nodelay;
limit_req_status 429;
proxy_pass http://web:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://web:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 65s;
}
}
requirements-prod.txt
-r requirements-mysql.txt
gunicorn==26.2.0
如何确认成功
访问 http://127.0.0.1:8010/、/admin/、/health/,页面和 CSS 都正常。第一次生产形态数据库可能为空,这是正常状态;使用后台建立分类和文章。seed_demo 会拒绝 DEBUG=False 环境,防止误把演示数据写入运营库。
docker compose logs --tail=80 web nginx
docker compose exec -T web python manage.py migrate --check
练习:上传图片,执行 docker compose up -d --force-recreate web,确认文章和图片仍在。解释为什么不能用 docker compose down -v 作为日常重启命令:-v 会删除命名卷。
常见问题
web 健康但首页出错? 健康检查只验证数据库连接,必须先执行迁移。权限 denied? 检查 init 是否成功,不要用 chmod 777 绕过问题。.env 在 Python 中自动加载了吗? 没有,Compose 将其中选定变量注入容器。新依赖未生效? 需要重新 build,单纯 restart 不会重建镜像。
官方参考
登录后可保存阅读位置、标记章节完成,并在其他设备继续学习。
登录并保存学习进度 →联系站长
微信:VIPdjango。技术问题建议先在 社区讨论 中交流,方便更多人参与和查阅。
章节讨论 0