From 19af18231496b30fbd06858644af54a454c3e2f9 Mon Sep 17 00:00:00 2001 From: Zhengen TANG Date: Tue, 18 Aug 2026 14:40:19 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=9F=E4=BA=A7=E7=8E=AF=E5=A2=83=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E5=87=86=E5=A4=87=EF=BC=9ADocker=20=E5=8C=96=20+=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AD=98=E5=82=A8=E9=85=8D=E7=BD=AE=20+=20SM?= =?UTF-8?q?TP=20=E9=82=AE=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 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_* 环境变量说明 --- .dockerignore | 25 +++++++++++++ .env.example | 14 +++++++ docker-compose.prod.yml | 61 +++++++++++++++++++++++++++++++ docker/Dockerfile | 61 +++++++++++++++++++++++++++++++ docker/entrypoint.sh | 16 ++++++++ requirements/production.txt | 1 + wagtailcms/settings/production.py | 43 +++++++++++++++++++--- 7 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 .dockerignore create mode 100644 docker-compose.prod.yml create mode 100644 docker/Dockerfile create mode 100644 docker/entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..92f801f --- /dev/null +++ b/.dockerignore @@ -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/ diff --git a/.env.example b/.env.example index 3758a79..b0931f4 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..c510bf1 --- /dev/null +++ b/docker-compose.prod.yml @@ -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: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..038cf21 --- /dev/null +++ b/docker/Dockerfile @@ -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"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..963d77c --- /dev/null +++ b/docker/entrypoint.sh @@ -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 "$@" diff --git a/requirements/production.txt b/requirements/production.txt index 3d3c5b1..19473fd 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -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 diff --git a/wagtailcms/settings/production.py b/wagtailcms/settings/production.py index 3eb4722..8c8ffa2 100644 --- a/wagtailcms/settings/production.py +++ b/wagtailcms/settings/production.py @@ -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,