# Sport Era Production Docker Compose Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 为 `/www/wwwroot/sbnews` 创建统一的生产 Docker Compose,使 API、定时任务、Crawler、KB Worker、Admin 构建版和 uni-app H5 构建版通过宝塔域名稳定运行。 **Architecture:** PHP、Scheduler、Crawler、KB Worker 和三个 Nginx 服务使用 host 网络,直接访问宿主机 MySQL、Redis及回环端口。Admin/H5 使用 Node 20 多阶段构建并由独立 Nginx 托管,宝塔只负责公网域名与 HTTPS 反向代理。 **Tech Stack:** Docker Compose v2、PHP 8.0 FPM、ThinkPHP、Nginx、Node.js 20、Vite、uni-app、Python 3.11、MySQL、Redis。 --- ## 文件职责 新增或修改的文件按职责分为以下单元: - 生产密钥边界:`.gitignore`、`docker/.env.server.production.example`、`docker/.env.crawler.production.example`、`server/config/cache.php`。 - H5 构建配置:`uniapp/src/config/index.ts`。 - PHP/API 运行时:`docker/config/php/conf.d/zz-opcache.prod.ini`、`docker/config/php/php-fpm.prod.conf`、`docker/config/nginx/conf.d/sport-era.prod-api.conf`。 - Admin 静态镜像:`docker/Dockerfile.admin`、`admin/.dockerignore`、`docker/config/nginx/static/admin.conf`。 - H5 静态镜像:`docker/Dockerfile.uniapp-h5`、`uniapp/.dockerignore`、`docker/config/nginx/static/uniapp-h5.conf`。 - 统一编排:`docker/docker-compose.prod.yml`。 - 运维交付:`docs/生产环境Docker部署说明.md`、`docs/业务进度管理.md`。 现有工作区包含其他未提交改动。每次提交前必须使用 `git status --short` 和 `git diff --cached --stat`,只暂存本任务列出的文件。 ### Task 1: 建立生产密钥边界并移除 Redis 密码回退 **Files:** - Modify: `.gitignore:35` - Create: `docker/.env.server.production.example` - Create: `docker/.env.crawler.production.example` - Modify: `server/config/cache.php:36` - [ ] **Step 1: 运行变更前静态检查** Run: ```powershell rg -n "\.env\.server\.production|\.env\.crawler\.production" .gitignore rg -n "'password' => env\('cache\.password', ''\)" server/config/cache.php ``` Expected: 两条命令都没有匹配;第一条退出码为 1,第二条退出码为 1。 - [ ] **Step 2: 忽略真实生产环境文件** 在 `.gitignore` 的 Docker 私有配置区域追加: ```gitignore docker/.env.server.production docker/.env.crawler.production ``` - [ ] **Step 3: 创建 PHP 生产环境模板** 创建 `docker/.env.server.production.example`: ```ini APP_DEBUG = false DASHSCOPE_API_KEY = change_me [APP] DEFAULT_TIMEZONE = Asia/Shanghai [DATABASE] TYPE = mysql HOSTNAME = 127.0.0.1 DATABASE = sbnews USERNAME = sbnews PASSWORD = change_me HOSTPORT = 3300 CHARSET = utf8mb4 DEBUG = false PREFIX = la_ [CACHE] DRIVER = redis HOST = 127.0.0.1 PORT = 6377 PASSWORD = change_me SELECT = 2 [LANG] default_lang = zh-cn [PROJECT] UNIQUE_IDENTIFICATION = likeadmin DEMO_ENV = false ``` - [ ] **Step 4: 创建 Crawler/KB Worker 生产环境模板** 创建 `docker/.env.crawler.production.example`: ```dotenv TZ=Asia/Shanghai DQD_DB_HOST=127.0.0.1 DQD_DB_PORT=3300 DQD_DB_NAME=sbnews DQD_DB_USER=sbnews DQD_DB_PASSWORD=change_me DQD_DB_PREFIX=la_ DQD_DB_CHARSET=utf8mb4 DQD_DB_POOL_SIZE=5 DQD_REDIS_HOST=127.0.0.1 DQD_REDIS_PORT=6377 DQD_REDIS_PASSWORD=change_me DQD_REDIS_DB=2 DQD_LOG_LEVEL=INFO DQD_LOG_FILE=/app/logs/crawler.log EMBEDDING_API_KEY=change_me EMBEDDING_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode EMBEDDING_MODEL=text-embedding-v4 EMBEDDING_DIM=1024 EMBEDDING_BATCH_SIZE=10 ``` - [ ] **Step 5: 清空 Redis 密码源码回退** 将 `server/config/cache.php` 中 Redis 密码配置改为: ```php // 密码 'password' => env('cache.password', ''), ``` 不要改动 Redis 默认主机、端口、前缀和数据库编号。 - [ ] **Step 6: 验证密钥边界** Run: ```powershell rg -n "docker/\.env\.server\.production|docker/\.env\.crawler\.production" .gitignore rg -n "'password' => env\('cache\.password', ''\)" server/config/cache.php rg -n "PASSWORD = change_me|DQD_DB_PASSWORD=change_me|DQD_REDIS_PASSWORD=change_me|EMBEDDING_API_KEY=change_me" docker/.env.server.production.example docker/.env.crawler.production.example git diff --check -- .gitignore server/config/cache.php docker/.env.server.production.example docker/.env.crawler.production.example ``` Expected: 忽略规则、空密码回退和所有模板占位符均匹配;`git diff --check` 无输出。 - [ ] **Step 7: 提交密钥边界变更** ```powershell git add -- .gitignore server/config/cache.php docker/.env.server.production.example docker/.env.crawler.production.example git diff --cached --stat git commit -m "chore: secure production runtime secrets" ``` Expected: 提交只包含上述四个文件。 - [ ] **Step 8: 按项目规定同步 Server 配置到测试服** Run: ```powershell Set-Location D:\www\gs-sport-era powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -AutoIncremental -AutoIncrementalCommits 3 -RemoteDir /www/wwwroot/test-server.sbnews.net ``` Expected: 部署脚本成功完成;测试服继续从私有 `.env.server` 获取 Redis 密码,接口不因默认值清空而中断。 ### Task 2: 让 uni-app H5 生产构建使用 API 构建变量 **Files:** - Modify: `uniapp/src/config/index.ts:1-18` - [ ] **Step 1: 确认当前配置未读取 Vite 环境变量** Run: ```powershell rg -n "import\.meta\.env\.VITE_APP_BASE_URL" uniapp/src/config/index.ts ``` Expected: 没有匹配,退出码为 1。 - [ ] **Step 2: 改写 API 基础地址初始化** 将 `uniapp/src/config/index.ts` 改为: ```ts const configuredBaseUrl = import.meta.env.VITE_APP_BASE_URL || 'https://test-server.sbnews.net' const envBaseUrl = configuredBaseUrl.replace(/\/+$/, '') let baseUrl = `${envBaseUrl}/` /* * 微信小程序在`VITE_APP_BASE_URL`存在或`dev`模式下 * 使用`VITE_APP_BASE_URL`的值 * 其他情况使用`[baseUrl]`,方便服务端替换 */ //#ifdef MP-WEIXIN baseUrl = `${envBaseUrl}/` //#endif const config = { version: '1.9.0', //版本号 baseUrl, //请求接口域名 urlPrefix: 'api', //请求默认前缀 timeout: 60 * 1000 //请求超时时长 } export default config ``` 默认值继续指向测试 API;生产 Docker 构建显式传入正式 API 地址。 - [ ] **Step 3: 静态验证 URL 规范化** Run: ```powershell rg -n "VITE_APP_BASE_URL|replace\(/\\/\+\$/|test-server\.sbnews\.net" uniapp/src/config/index.ts git diff --check -- uniapp/src/config/index.ts ``` Expected: 三项配置均匹配;`git diff --check` 无输出。 - [ ] **Step 4: 提交 H5 构建配置** ```powershell git add -- uniapp/src/config/index.ts git diff --cached --stat git commit -m "build: make h5 api url environment driven" ``` Expected: 提交只包含 `uniapp/src/config/index.ts`。按项目边界不自动执行 H5 构建。 ### Task 3: 创建生产 PHP-FPM、OPcache 与 API Nginx 配置 **Files:** - Create: `docker/config/php/conf.d/zz-opcache.prod.ini` - Create: `docker/config/php/php-fpm.prod.conf` - Create: `docker/config/nginx/conf.d/sport-era.prod-api.conf` - [ ] **Step 1: 创建生产 OPcache 配置** 创建 `docker/config/php/conf.d/zz-opcache.prod.ini`: ```ini opcache.enable=1 opcache.enable_cli=1 opcache.memory_consumption=256 opcache.interned_strings_buffer=32 opcache.max_accelerated_files=30000 opcache.validate_timestamps=1 opcache.revalidate_freq=2 opcache.save_comments=1 ``` 保持时间戳检查,因为生产 Server 使用源码挂载;`git pull` 后无需依赖镜像层变化才能刷新 PHP 字节码。 - [ ] **Step 2: 将 PHP-FPM 限制到宿主机回环地址** 创建 `docker/config/php/php-fpm.prod.conf`: ```ini [www] listen = 127.0.0.1:9000 listen.allowed_clients = 127.0.0.1 ``` - [ ] **Step 3: 创建 API Nginx 配置** 创建 `docker/config/nginx/conf.d/sport-era.prod-api.conf`: ```nginx server { listen 127.0.0.1:8000; server_name api.sbnews.net; root /likeadmin_php/server/public; index index.php index.html; client_max_body_size 20M; access_log /logs/sport-era-prod_access.log; error_log /logs/sport-era-prod_error.log; set_real_ip_from 127.0.0.1; real_ip_header X-Forwarded-For; real_ip_recursive on; location = /healthz { access_log off; default_type text/plain; return 200 "ok\n"; } location / { try_files $uri $uri/ /index.php?s=$uri&$args; } location ~ \.php$ { try_files $uri =404; include fastcgi_params; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; fastcgi_read_timeout 300s; } location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|ico|webp|woff2?)$ { expires 7d; add_header Cache-Control "public"; try_files $uri =404; } location ~ /\. { deny all; } location = /favicon.ico { log_not_found off; access_log off; } } ``` - [ ] **Step 4: 静态检查回环监听与 FastCGI 目标** Run: ```powershell rg -n "validate_timestamps=1|revalidate_freq=2" docker/config/php/conf.d/zz-opcache.prod.ini rg -n "listen = 127\.0\.0\.1:9000|allowed_clients = 127\.0\.0\.1" docker/config/php/php-fpm.prod.conf rg -n 'listen 127\.0\.0\.1:8000|fastcgi_pass 127\.0\.0\.1:9000|location = /healthz|try_files \$uri' docker/config/nginx/conf.d/sport-era.prod-api.conf git diff --check -- docker/config/php/conf.d/zz-opcache.prod.ini docker/config/php/php-fpm.prod.conf docker/config/nginx/conf.d/sport-era.prod-api.conf ``` Expected: 所有安全监听和路由配置均匹配;`git diff --check` 无输出。 - [ ] **Step 5: 提交生产 PHP/API 配置** ```powershell git add -- docker/config/php/conf.d/zz-opcache.prod.ini docker/config/php/php-fpm.prod.conf docker/config/nginx/conf.d/sport-era.prod-api.conf git diff --cached --stat git commit -m "build: add production php and api runtime" ``` Expected: 提交只包含三个生产运行配置文件。 ### Task 4: 创建 Admin 与 H5 多阶段静态镜像 **Files:** - Create: `admin/.dockerignore` - Create: `uniapp/.dockerignore` - Create: `docker/Dockerfile.admin` - Create: `docker/Dockerfile.uniapp-h5` - Create: `docker/config/nginx/static/admin.conf` - Create: `docker/config/nginx/static/uniapp-h5.conf` - [ ] **Step 1: 创建 Admin 构建上下文忽略文件** 创建 `admin/.dockerignore`: ```dockerignore node_modules dist dist-ssr .git .env .env.* npm-debug.log* yarn-debug.log* yarn-error.log* ``` - [ ] **Step 2: 创建 H5 构建上下文忽略文件** 创建 `uniapp/.dockerignore`: ```dockerignore node_modules dist dist-ssr unpackage .hbuilderx .git .env .env.* npm-debug.log* yarn-debug.log* yarn-error.log* ``` - [ ] **Step 3: 创建 Admin 多阶段 Dockerfile** 创建 `docker/Dockerfile.admin`: ```dockerfile # syntax=docker/dockerfile:1 FROM node:20-alpine AS build WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --no-audit --no-fund COPY . . ARG VITE_APP_BASE_URL=https://api.sbnews.net ENV VITE_APP_BASE_URL=${VITE_APP_BASE_URL} RUN npx vite build FROM registry.cn-guangzhou.aliyuncs.com/likeadmin/nginx:1.23.1 COPY --from=build /app/dist/ /usr/share/nginx/html/ ``` - [ ] **Step 4: 创建 uni-app H5 多阶段 Dockerfile** 创建 `docker/Dockerfile.uniapp-h5`: ```dockerfile # syntax=docker/dockerfile:1 FROM node:20-alpine AS build WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --legacy-peer-deps --no-audit --no-fund COPY . . ARG VITE_APP_BASE_URL=https://api.sbnews.net ENV VITE_APP_BASE_URL=${VITE_APP_BASE_URL} \ UNIAPP_RELEASE_PATH=/tmp/mobile-release \ NODE_OPTIONS=--max-old-space-size=4096 RUN npm run build:h5 FROM registry.cn-guangzhou.aliyuncs.com/likeadmin/nginx:1.23.1 COPY --from=build /app/dist/build/h5/ /usr/share/nginx/html/ ``` - [ ] **Step 5: 创建 Admin 静态 Nginx 配置** 创建 `docker/config/nginx/static/admin.conf`: ```nginx server { listen 127.0.0.1:5176; server_name keislwo.sbnews.net; root /usr/share/nginx/html; index index.html; access_log /var/log/nginx/admin_access.log; error_log /var/log/nginx/admin_error.log; location = /healthz { access_log off; default_type text/plain; return 200 "ok\n"; } location = /index.html { add_header Cache-Control "no-cache"; } location / { try_files $uri $uri/ /index.html; } location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|ico|webp|woff2?)$ { expires 7d; add_header Cache-Control "public, immutable"; try_files $uri =404; } } ``` - [ ] **Step 6: 创建 H5 静态 Nginx 配置** 创建 `docker/config/nginx/static/uniapp-h5.conf`: ```nginx server { listen 127.0.0.1:5177; server_name web.sbnews.net; root /usr/share/nginx/html; index index.html; access_log /var/log/nginx/h5_access.log; error_log /var/log/nginx/h5_error.log; location = /healthz { access_log off; default_type text/plain; return 200 "ok\n"; } location = /index.html { add_header Cache-Control "no-cache"; } location / { try_files $uri $uri/ /index.html; } location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|ico|webp|woff2?)$ { expires 7d; add_header Cache-Control "public, immutable"; try_files $uri =404; } } ``` - [ ] **Step 7: 静态检查镜像输出路径、生产域名和 SPA 回退** Run: ```powershell rg -n "node:20-alpine|VITE_APP_BASE_URL=https://api\.sbnews\.net|/app/dist" docker/Dockerfile.admin docker/Dockerfile.uniapp-h5 rg -n 'listen 127\.0\.0\.1:5176|try_files \$uri \$uri/ /index\.html' docker/config/nginx/static/admin.conf rg -n 'listen 127\.0\.0\.1:5177|try_files \$uri \$uri/ /index\.html' docker/config/nginx/static/uniapp-h5.conf git diff --check -- admin/.dockerignore uniapp/.dockerignore docker/Dockerfile.admin docker/Dockerfile.uniapp-h5 docker/config/nginx/static/admin.conf docker/config/nginx/static/uniapp-h5.conf ``` Expected: 两个镜像、两个监听地址、两个 SPA 回退均匹配;不执行完整前端构建。 - [ ] **Step 8: 提交前端生产镜像** ```powershell git add -- admin/.dockerignore uniapp/.dockerignore docker/Dockerfile.admin docker/Dockerfile.uniapp-h5 docker/config/nginx/static/admin.conf docker/config/nginx/static/uniapp-h5.conf git diff --cached --stat git commit -m "build: add production frontend images" ``` Expected: 提交只包含六个前端容器相关文件。 ### Task 5: 创建统一生产 Compose **Files:** - Create: `docker/docker-compose.prod.yml` - [ ] **Step 1: 创建生产 Compose 文件** 创建 `docker/docker-compose.prod.yml`: ```yaml name: sport-era-prod x-logging: &default-logging driver: json-file options: max-size: "20m" max-file: "5" x-php-service: &php-service image: sport-era-prod-php:local build: context: .. dockerfile: docker/Dockerfile.php restart: unless-stopped network_mode: host working_dir: /likeadmin_php/server environment: TZ: Asia/Shanghai volumes: - ${SERVER_DIR:-../server}:/likeadmin_php/server - ${SERVER_ENV_FILE:-./.env.server.production}:/likeadmin_php/server/.env:ro - ./config/php/conf.d/zz-opcache.prod.ini:/usr/local/etc/php/conf.d/zz-opcache.ini:ro - ./config/php/php-fpm.prod.conf:/usr/local/etc/php-fpm.d/zz-production.conf:ro logging: *default-logging x-crawler-service: &crawler-service image: sport-era-prod-crawler:local build: context: ${CRAWLER_DIR:-./crawler} dockerfile: Dockerfile restart: unless-stopped network_mode: host env_file: - ${CRAWLER_ENV_FILE:-./.env.crawler.production} environment: TZ: Asia/Shanghai DQD_CONFIG_PATH: /app/config/settings.yaml DQD_TASKS_FILE: /app/config/crawler_tasks.yaml DQD_LOCK_DIR: /app/data/locks DQD_DB_HOST: 127.0.0.1 DQD_REDIS_HOST: 127.0.0.1 volumes: - ${CRAWLER_DIR:-./crawler}/logs:/app/logs - ${CRAWLER_DIR:-./crawler}/data:/app/data logging: *default-logging services: server: <<: *php-service healthcheck: test: - CMD-SHELL - >- php -r '$$socket=@fsockopen("127.0.0.1",9000,$$errno,$$errstr,2); exit($$socket ? 0 : 1);' interval: 15s timeout: 5s retries: 5 start_period: 20s scheduler: <<: *php-service command: - sh - -c - while true; do php think crontab; sleep 60; done crawler: <<: *crawler-service kb-worker: <<: *crawler-service command: - python - scripts/kb_worker.py - --batch=50 - --block-ms=5000 - --max-retries=3 api-nginx: image: registry.cn-guangzhou.aliyuncs.com/likeadmin/nginx:1.23.1 restart: unless-stopped network_mode: host depends_on: server: condition: service_healthy volumes: - ${SERVER_DIR:-../server}:/likeadmin_php/server:ro - ./config/nginx/conf.d/sport-era.prod-api.conf:/etc/nginx/conf.d/default.conf:ro - ./log/nginx/prod-logs:/logs healthcheck: test: - CMD-SHELL - wget -qO- http://127.0.0.1:8000/healthz | grep -q '^ok$' interval: 15s timeout: 5s retries: 5 start_period: 10s logging: *default-logging admin: image: sport-era-prod-admin:local build: context: ../admin dockerfile: ../docker/Dockerfile.admin args: VITE_APP_BASE_URL: ${PROD_API_BASE_URL:-https://api.sbnews.net} restart: unless-stopped network_mode: host volumes: - ./config/nginx/static/admin.conf:/etc/nginx/conf.d/default.conf:ro healthcheck: test: - CMD-SHELL - wget -qO- http://127.0.0.1:5176/healthz | grep -q '^ok$' interval: 15s timeout: 5s retries: 5 start_period: 10s logging: *default-logging uniapp-h5: image: sport-era-prod-uniapp-h5:local build: context: ../uniapp dockerfile: ../docker/Dockerfile.uniapp-h5 args: VITE_APP_BASE_URL: ${PROD_API_BASE_URL:-https://api.sbnews.net} restart: unless-stopped network_mode: host volumes: - ./config/nginx/static/uniapp-h5.conf:/etc/nginx/conf.d/default.conf:ro healthcheck: test: - CMD-SHELL - wget -qO- http://127.0.0.1:5177/healthz | grep -q '^ok$' interval: 15s timeout: 5s retries: 5 start_period: 10s logging: *default-logging ``` - [ ] **Step 2: 用示例环境文件展开 Compose** Run: ```powershell $env:SERVER_ENV_FILE = './.env.server.production.example' $env:CRAWLER_ENV_FILE = './.env.crawler.production.example' docker compose -f docker/docker-compose.prod.yml config -q $compose = docker compose -f docker/docker-compose.prod.yml config $compose | Select-String '127.0.0.1:8000|127.0.0.1:5176|127.0.0.1:5177|https://api.sbnews.net|scripts/kb_worker.py' Remove-Item Env:SERVER_ENV_FILE -ErrorAction SilentlyContinue Remove-Item Env:CRAWLER_ENV_FILE -ErrorAction SilentlyContinue ``` Expected: `config -q` 无输出且退出码为 0;展开内容包含三个回环入口、正式 API 域名和 KB Worker 命令。 - [ ] **Step 3: 确认 Compose 没有数据库服务与公网端口映射** Run: ```powershell $env:SERVER_ENV_FILE = './.env.server.production.example' $env:CRAWLER_ENV_FILE = './.env.crawler.production.example' $compose = docker compose -f docker/docker-compose.prod.yml config $compose | Select-String '^ (mysql|redis):' -CaseSensitive $compose | Select-String '^ ports:' -CaseSensitive Remove-Item Env:SERVER_ENV_FILE -ErrorAction SilentlyContinue Remove-Item Env:CRAWLER_ENV_FILE -ErrorAction SilentlyContinue ``` Expected: 两次 `Select-String` 都没有输出,因为生产 Compose 不创建 MySQL/Redis,也不使用 Docker `ports` 发布端口。 - [ ] **Step 4: 提交统一生产 Compose** ```powershell git add -- docker/docker-compose.prod.yml git diff --cached --stat git commit -m "build: add unified production compose" ``` Expected: 提交只包含 `docker/docker-compose.prod.yml`。 ### Task 6: 编写生产部署与宝塔反向代理说明 **Files:** - Create: `docs/生产环境Docker部署说明.md` - Modify: `docs/业务进度管理.md` - [ ] **Step 1: 创建生产部署说明** 创建 `docs/生产环境Docker部署说明.md`,内容必须包含以下完整章节与命令。 文件开头: ```markdown # 生产环境 Docker 部署说明 ## 环境边界 - 生产目录:`/www/wwwroot/sbnews` - Git 分支:`dev.1.0.0` - API:`api.sbnews.net` - Admin:`keislwo.sbnews.net` - H5:`web.sbnews.net` - MySQL、Redis、域名和 HTTPS 由宿主机/宝塔管理。 - Docker Compose 负责 Server、Scheduler、Crawler、KB Worker、Admin 和 H5。 ``` 首次部署命令: ```bash cd /www/wwwroot cd /www/wwwroot/sbnews git switch dev.1.0.0 git pull --ff-only origin dev.1.0.0 cp docker/.env.server.production.example docker/.env.server.production cp docker/.env.crawler.production.example docker/.env.crawler.production chmod 600 docker/.env.server.production docker/.env.crawler.production # 使用编辑器填写真实密码及 API Key,不要把真实配置提交到 Git。 sudo docker compose -f docker/docker-compose.prod.yml config -q sudo docker compose -f docker/docker-compose.prod.yml up -d --build ``` 日常更新命令: ```bash cd /www/wwwroot/sbnews git status --short git tag -f production-stable "$(git rev-parse HEAD)" git switch dev.1.0.0 git pull --ff-only origin dev.1.0.0 sudo docker compose -f docker/docker-compose.prod.yml config -q sudo docker compose -f docker/docker-compose.prod.yml up -d --build ``` 三个宝塔站点的反向代理核心配置分别为: ```nginx # api.sbnews.net location / { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; 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_read_timeout 300s; proxy_send_timeout 300s; } ``` ```nginx # keislwo.sbnews.net location / { proxy_pass http://127.0.0.1:5176; proxy_http_version 1.1; 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; } ``` ```nginx # web.sbnews.net location / { proxy_pass http://127.0.0.1:5177; proxy_http_version 1.1; 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; } ``` 验收命令: ```bash sudo docker compose -f docker/docker-compose.prod.yml ps curl -f http://127.0.0.1:8000/api/index/config curl -I http://127.0.0.1:5176/ curl -I http://127.0.0.1:5177/ curl -I https://api.sbnews.net/api/index/config curl -I https://keislwo.sbnews.net/ curl -I https://web.sbnews.net/ ``` 日志和回滚命令: ```bash sudo docker compose -f docker/docker-compose.prod.yml logs --tail 100 server api-nginx scheduler crawler kb-worker admin uniapp-h5 git status --short git switch --detach production-stable sudo docker compose -f docker/docker-compose.prod.yml up -d --build # 回到生产分支 git switch dev.1.0.0 ``` 文档还必须明确:不自动导入 SQL、不自动修改宿主机 MySQL/Redis、不在 Git 中保存真实密钥、密码公开后应轮换。 - [ ] **Step 2: 在业务进度中追加生产编排记录** 在 `docs/业务进度管理.md` 的“已完成事项”顶部追加: ```markdown ### 100. 生产环境统一 Docker Compose - 状态:配置已完成,待生产服务器部署验收 - 时间:2026-08-02 完成内容: - 新增生产 Compose,统一运行 Server、API Nginx、Scheduler、Crawler、KB Worker、Admin 构建版和 uni-app H5 构建版。 - 生产容器使用 host 网络连接宿主机 MySQL `3300` 与 Redis `6377/DB2`,数据库和缓存继续由宿主机管理。 - Admin 与 H5 使用 Node 多阶段构建和独立静态 Nginx,由宝塔分别反向代理三个正式域名。 - 新增生产私有环境模板、回环端口限制、健康检查、日志轮转、部署验证和回滚说明。 涉及模块: - `docker/docker-compose.prod.yml` - `docker/Dockerfile.admin` - `docker/Dockerfile.uniapp-h5` - `docker/config/nginx/` - `docker/config/php/` - `docs/生产环境Docker部署说明.md` ``` 保留该文件现有未提交内容,不覆盖其他协作者的进度记录。 - [ ] **Step 3: 验证文档不包含真实密钥** Run: ```powershell rg -n "change_me|\.env\.server\.production|\.env\.crawler\.production|127\.0\.0\.1:(8000|5176|5177)" docs/生产环境Docker部署说明.md rg -n "生产环境统一 Docker Compose|docker-compose\.prod\.yml" docs/业务进度管理.md git diff --check -- docs/生产环境Docker部署说明.md docs/业务进度管理.md ``` Expected: 文档中的模板、端口和进度记录均匹配;`git diff --check` 无输出。 - [ ] **Step 4: 提交部署说明,谨慎处理已有进度文档改动** 先运行: ```powershell git diff -- docs/业务进度管理.md git status --short ``` 如果 `docs/业务进度管理.md` 仍包含其他协作者的未提交内容,只提交新的独立部署说明: ```powershell git add -- docs/生产环境Docker部署说明.md git diff --cached --stat git commit -m "docs: add production docker deployment guide" ``` 此时保留业务进度文档中的新增记录在工作区,不要把其他协作者的内容混入提交。如果该文件在执行时已经干净,则同时暂存并提交它。 ### Task 7: 最终静态评审与交付 **Files:** - Review: `.gitignore` - Review: `server/config/cache.php` - Review: `uniapp/src/config/index.ts` - Review: `docker/docker-compose.prod.yml` - Review: `docker/Dockerfile.admin` - Review: `docker/Dockerfile.uniapp-h5` - Review: `docker/config/nginx/conf.d/sport-era.prod-api.conf` - Review: `docker/config/nginx/static/admin.conf` - Review: `docker/config/nginx/static/uniapp-h5.conf` - Review: `docker/config/php/conf.d/zz-opcache.prod.ini` - Review: `docker/config/php/php-fpm.prod.conf` - Review: `docker/.env.server.production.example` - Review: `docker/.env.crawler.production.example` - Review: `docs/生产环境Docker部署说明.md` - [ ] **Step 1: 展开最终 Compose** Run: ```powershell $env:SERVER_ENV_FILE = './.env.server.production.example' $env:CRAWLER_ENV_FILE = './.env.crawler.production.example' docker compose -f docker/docker-compose.prod.yml config -q docker compose -f docker/docker-compose.prod.yml config --services Remove-Item Env:SERVER_ENV_FILE -ErrorAction SilentlyContinue Remove-Item Env:CRAWLER_ENV_FILE -ErrorAction SilentlyContinue ``` Expected services: ```text server scheduler crawler kb-worker api-nginx admin uniapp-h5 ``` - [ ] **Step 2: 检查正式域名与测试域名边界** Run: ```powershell rg -n "api\.sbnews\.net|keislwo\.sbnews\.net|web\.sbnews\.net" docker docker/Dockerfile.admin docker/Dockerfile.uniapp-h5 docs/生产环境Docker部署说明.md rg -n "test-server\.sbnews\.net" docker/docker-compose.prod.yml docker/Dockerfile.admin docker/Dockerfile.uniapp-h5 docker/config/nginx docs/生产环境Docker部署说明.md ``` Expected: 正式域名存在;生产 Docker 与部署文档中没有测试 API 域名。`uniapp/src/config/index.ts` 可以保留测试域名作为未传构建变量时的开发默认值。 - [ ] **Step 3: 检查真实密钥没有进入本次文件** Run: ```powershell rg -n --pcre2 "(?i)(password|api_key)\s*=\s*(?!change_me|your_|$)[^#\s]+" docker/.env.server.production.example docker/.env.crawler.production.example docs/生产环境Docker部署说明.md ``` Expected: 没有匹配,退出码为 1。 - [ ] **Step 4: 检查差异、提交和无关工作区内容** Run: ```powershell git diff --check git status --short git log -8 --oneline --decorate ``` Expected: `git diff --check` 无输出;本任务真实源码和运行配置均已有独立提交;原有社区、赛事或其他协作者改动仍保持原状态且未混入本任务提交。 - [ ] **Step 5: 交付服务器执行命令,不在本机自动构建** 最终交付应明确生产服务器执行: ```bash cd /www/wwwroot/sbnews git switch dev.1.0.0 git pull --ff-only origin dev.1.0.0 sudo docker compose -f docker/docker-compose.prod.yml config -q sudo docker compose -f docker/docker-compose.prod.yml up -d --build ``` 本地任务到此结束,不自动执行 Admin/H5 完整构建,不自动连接或修改生产数据库,也不自动操作宝塔配置。