生产环境部署准备:Docker 化 + 修复存储配置 + SMTP 邮件

- 修复 production.py 存储配置:Django 6.0 已移除 DEFAULT_FILE_STORAGE/
  STATICFILES_STORAGE 旧式设置兼容层,改用 STORAGES 字典(此前的写法
  实际上早已失效,属于遗留 bug)
- 静态文件接入 whitenoise(压缩+哈希文件名+长缓存头),容器化部署下
  无需额外 Nginx/CDN 即可正确提供静态文件
- 新增生产 SMTP 邮件配置(EMAIL_HOST/PORT/USER/PASSWORD/USE_TLS/
  DEFAULT_FROM_EMAIL),修复线索通知邮件/PIPL 删除确认邮件在生产环境
  发不出去的问题
- 新增 docker/Dockerfile(多阶段构建,非 root 用户运行)
- 新增 docker/entrypoint.sh(启动时自动 migrate + collectstatic +
  bootstrap_deployment,均为幂等操作)
- 新增 docker-compose.prod.yml(backend + postgres + redis 编排)
- 新增 .dockerignore
- .env.example 补充 POSTGRES_*/EMAIL_* 环境变量说明
This commit is contained in:
2026-08-18 14:40:19 +08:00
parent 306068bc22
commit 19af182314
7 changed files with 216 additions and 5 deletions
+25
View File
@@ -0,0 +1,25 @@
# 版本控制/编辑器相关
.git/
.gitignore
.vscode/
**/__pycache__/
*.pyc
# 前端是独立仓库,后端镜像构建不需要
frontend/
# 本地开发数据/产物,不应打进镜像(容器内由 entrypoint 重新生成或挂载卷提供)
db.sqlite3
staticfiles/
media/
.env
.env.*
!.env.example
# 文档/说明,非运行时依赖
documents/
README.md
# OpenSearch PoC 脚手架,与后端应用镜像无关
docker-compose.opensearch.yml
docker/opensearch/
+14
View File
@@ -7,9 +7,23 @@ WAGTAILADMIN_BASE_URL=https://cms.example.com
# 数据库(生产环境,PostgreSQL)
DATABASE_URL=postgres://user:password@postgres:5432/enterprise_cms
# docker-compose.prod.yml 中 postgres 容器初始化用(需与上面 DATABASE_URL 里的
# 用户名/密码/库名保持一致,否则 backend 连接不上自己容器编排出来的数据库)
POSTGRES_DB=enterprise_cms
POSTGRES_USER=user
POSTGRES_PASSWORD=change-me
# 缓存
REDIS_URL=redis://redis:6379/1
# 生产环境邮件发送(SMTP),用于线索通知邮件、PIPL 数据删除确认邮件
EMAIL_HOST=smtp.example.com
EMAIL_PORT=587
EMAIL_HOST_USER=user@example.com
EMAIL_HOST_PASSWORD=change-me
EMAIL_USE_TLS=True
DEFAULT_FROM_EMAIL=user@example.com
# 跨域 / CSRF 白名单(Next.js 前端域名)
CORS_ALLOWED_ORIGINS=https://www.example.com
CSRF_TRUSTED_ORIGINS=https://www.example.com
+61
View File
@@ -0,0 +1,61 @@
# 生产/预发环境编排:后端(Django+Wagtail+gunicorn)+ PostgreSQL + Redis。
# 前端 Next.js 是独立的 git 仓库,通常单独部署(Vercel / 自建 Node 容器),不在本 compose 编排范围内。
#
# 使用方式:
# 1. 复制 .env.example 为 .env 并按实际生产环境填写(尤其 SECRET_KEY / 数据库密码 / OSS 密钥等)。
# 2. docker compose -f docker-compose.prod.yml up -d --build
# 3. 首次启动会自动执行 migrate / collectstatic / bootstrap_deployment(见 docker/entrypoint.sh)。
#
# 生产环境建议在本 compose 前面再挂一层反向代理(Nginx/云厂商 SLB/CLB)做 TLS 终止,
# 并将 SECURE_PROXY_SSL_HEADER 对应的转发头传递给 backend 服务。
services:
backend:
build:
context: .
dockerfile: docker/Dockerfile
restart: unless-stopped
env_file:
- .env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "8000:8000"
volumes:
- static_data:/app/staticfiles
- media_data:/app/media
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-enterprise_cms}
POSTGRES_USER: ${POSTGRES_USER:-user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required, please set it in .env}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-user} -d ${POSTGRES_DB:-enterprise_cms}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
redis_data:
static_data:
media_data:
+61
View File
@@ -0,0 +1,61 @@
# syntax=docker/dockerfile:1
# 后端(Django + Wagtail)生产镜像,多阶段构建:
# builder — 安装编译依赖 + Python 包(含需要本地编译的 psycopg/Pillow 等)
# runtime — 仅包含运行时所需的系统库 + 已安装的 Python 包,体积更小,非 root 用户运行
ARG PYTHON_VERSION=3.14-slim
FROM python:${PYTHON_VERSION} AS builder
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /app
# 编译期依赖:psycopg(PostgreSQL 客户端)、Pillow(Wagtail 图片处理)均需要这些头文件/编译工具
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
libjpeg-dev \
zlib1g-dev \
libwebp-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements/ requirements/
RUN pip install --user --no-warn-script-location -r requirements/production.txt
FROM python:${PYTHON_VERSION} AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH=/home/django/.local/bin:$PATH \
DJANGO_SETTINGS_MODULE=wagtailcms.settings.production
WORKDIR /app
# 运行期依赖:只需要动态库,不需要编译工具链
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
libjpeg62-turbo \
libwebp7 \
zlib1g \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd -r django && useradd -r -g django -m -d /home/django django
COPY --from=builder /root/.local /home/django/.local
COPY . .
COPY docker/entrypoint.sh /app/docker/entrypoint.sh
RUN chmod +x /app/docker/entrypoint.sh \
&& mkdir -p /app/staticfiles /app/media \
&& chown -R django:django /app /home/django/.local
USER django
EXPOSE 8000
ENTRYPOINT ["/app/docker/entrypoint.sh"]
CMD ["gunicorn", "wagtailcms.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# 容器启动入口:先完成一次性初始化步骤,再把控制权交给 CMD(默认是 gunicorn)。
# 全部步骤均设计为幂等,可安全地在每次容器启动/滚动升级时重复执行。
set -e
echo "[entrypoint] running database migrations..."
python manage.py migrate --noinput
echo "[entrypoint] collecting static files..."
python manage.py collectstatic --noinput
echo "[entrypoint] bootstrap deployment (superuser + RBAC groups)..."
python manage.py bootstrap_deployment
echo "[entrypoint] starting: $*"
exec "$@"
+1
View File
@@ -4,3 +4,4 @@ psycopg[binary]>=3.2
django-storages[s3]>=1.14
gunicorn>=23.0
sentry-sdk>=2.0
whitenoise>=6.7,<7.0
+38 -5
View File
@@ -8,6 +8,19 @@ DATABASES = {
"default": env.db("DATABASE_URL"),
}
# 生产环境邮件发送(线索通知邮件、PIPL 数据删除确认邮件均依赖此配置,见 apps/forms/views.py)。
# dev.py 用的是 console backend(打印到终端),生产环境必须换成真实 SMTP,否则邮件静默发送失败
# (send_mail 调用处未加 fail_silently,连接失败会抛异常;发信人账号密码等敏感信息只从 .env 读取,
# 不写入源码)。
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = env("EMAIL_HOST", default="smtp.shiytech.cn")
EMAIL_PORT = env.int("EMAIL_PORT", default=587)
EMAIL_HOST_USER = env("EMAIL_HOST_USER", default="")
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD", default="")
EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", default=True)
DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL", default=EMAIL_HOST_USER)
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
@@ -31,11 +44,22 @@ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[])
# 国内对象存储(阿里云 OSS / 腾讯云 COS 均兼容 S3 协议)
DEFAULT_FILE_STORAGE = env(
"DEFAULT_FILE_STORAGE",
default="django.core.files.storage.FileSystemStorage",
)
# Django 6.0 已移除 DEFAULT_FILE_STORAGE/STATICFILES_STORAGE 旧式设置的兼容层,
# 必须通过 STORAGES 字典配置(此前用旧式 DEFAULT_FILE_STORAGE 赋值实际上早已不生效,属于遗留 bug)。
# 媒体文件:国内对象存储(阿里云 OSS / 腾讯云 COS 均兼容 S3 协议),留空则回退本地文件系统。
# 静态文件:交给 whitenoise 在应用进程内直接托管(带哈希文件名 + gzip/brotli 压缩 + 长效缓存头),
# 容器化部署场景下无需额外的 Nginx/CDN 即可正确提供 STATIC_ROOT 下的文件。
STORAGES = {
"default": {
"BACKEND": env(
"DEFAULT_FILE_STORAGE",
default="django.core.files.storage.FileSystemStorage",
),
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
AWS_ACCESS_KEY_ID = env("OSS_ACCESS_KEY_ID", default="")
AWS_SECRET_ACCESS_KEY = env("OSS_SECRET_ACCESS_KEY", default="")
AWS_STORAGE_BUCKET_NAME = env("OSS_BUCKET_NAME", default="")
@@ -44,6 +68,15 @@ AWS_S3_ENDPOINT_URL = env("OSS_ENDPOINT_URL", default="")
# 配置后媒体文件 URL 会替换成该加速域名,避免直接暴露 OSS 源站地址(详见 §2.15)。
AWS_S3_CUSTOM_DOMAIN = env("OSS_CDN_DOMAIN", default="")
# whitenoise 需要紧跟在 SecurityMiddleware 之后,才能在其它中间件之前提供静态文件响应。
_security_middleware = "django.middleware.security.SecurityMiddleware"
_security_index = MIDDLEWARE.index(_security_middleware)
MIDDLEWARE = [
*MIDDLEWARE[: _security_index + 1],
"whitenoise.middleware.WhiteNoiseMiddleware",
*MIDDLEWARE[_security_index + 1 :],
]
LOGGING = {
"version": 1,
"disable_existing_loggers": False,