生产环境部署准备: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:
@@ -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/
|
||||||
@@ -7,9 +7,23 @@ WAGTAILADMIN_BASE_URL=https://cms.example.com
|
|||||||
# 数据库(生产环境,PostgreSQL)
|
# 数据库(生产环境,PostgreSQL)
|
||||||
DATABASE_URL=postgres://user:password@postgres:5432/enterprise_cms
|
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
|
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 前端域名)
|
# 跨域 / CSRF 白名单(Next.js 前端域名)
|
||||||
CORS_ALLOWED_ORIGINS=https://www.example.com
|
CORS_ALLOWED_ORIGINS=https://www.example.com
|
||||||
CSRF_TRUSTED_ORIGINS=https://www.example.com
|
CSRF_TRUSTED_ORIGINS=https://www.example.com
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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"]
|
||||||
@@ -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 "$@"
|
||||||
@@ -4,3 +4,4 @@ psycopg[binary]>=3.2
|
|||||||
django-storages[s3]>=1.14
|
django-storages[s3]>=1.14
|
||||||
gunicorn>=23.0
|
gunicorn>=23.0
|
||||||
sentry-sdk>=2.0
|
sentry-sdk>=2.0
|
||||||
|
whitenoise>=6.7,<7.0
|
||||||
|
|||||||
@@ -8,6 +8,19 @@ DATABASES = {
|
|||||||
"default": env.db("DATABASE_URL"),
|
"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 = {
|
CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django.core.cache.backends.redis.RedisCache",
|
"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=[])
|
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[])
|
||||||
|
|
||||||
# 国内对象存储(阿里云 OSS / 腾讯云 COS 均兼容 S3 协议)
|
# Django 6.0 已移除 DEFAULT_FILE_STORAGE/STATICFILES_STORAGE 旧式设置的兼容层,
|
||||||
DEFAULT_FILE_STORAGE = env(
|
# 必须通过 STORAGES 字典配置(此前用旧式 DEFAULT_FILE_STORAGE 赋值实际上早已不生效,属于遗留 bug)。
|
||||||
"DEFAULT_FILE_STORAGE",
|
# 媒体文件:国内对象存储(阿里云 OSS / 腾讯云 COS 均兼容 S3 协议),留空则回退本地文件系统。
|
||||||
default="django.core.files.storage.FileSystemStorage",
|
# 静态文件:交给 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_ACCESS_KEY_ID = env("OSS_ACCESS_KEY_ID", default="")
|
||||||
AWS_SECRET_ACCESS_KEY = env("OSS_SECRET_ACCESS_KEY", default="")
|
AWS_SECRET_ACCESS_KEY = env("OSS_SECRET_ACCESS_KEY", default="")
|
||||||
AWS_STORAGE_BUCKET_NAME = env("OSS_BUCKET_NAME", 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)。
|
# 配置后媒体文件 URL 会替换成该加速域名,避免直接暴露 OSS 源站地址(详见 §2.15)。
|
||||||
AWS_S3_CUSTOM_DOMAIN = env("OSS_CDN_DOMAIN", default="")
|
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 = {
|
LOGGING = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"disable_existing_loggers": False,
|
"disable_existing_loggers": False,
|
||||||
|
|||||||
Reference in New Issue
Block a user