diff --git a/docs/superpowers/plans/2026-08-02-production-docker-compose.md b/docs/superpowers/plans/2026-08-02-production-docker-compose.md new file mode 100644 index 0000000..5aabce9 --- /dev/null +++ b/docs/superpowers/plans/2026-08-02-production-docker-compose.md @@ -0,0 +1,1008 @@ +# 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 完整构建,不自动连接或修改生产数据库,也不自动操作宝塔配置。 diff --git a/docs/生产环境Docker部署说明.md b/docs/生产环境Docker部署说明.md new file mode 100644 index 0000000..6afc7f2 --- /dev/null +++ b/docs/生产环境Docker部署说明.md @@ -0,0 +1,145 @@ +# 生产环境 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。 + +生产 MySQL 和 Redis 不放入 Compose: + +- MySQL:宿主机 `127.0.0.1:3300`,数据库 `sbnews`。 +- Redis:宿主机 `127.0.0.1:6377`,使用 DB 2。 +- 实际密码只写入 `docker/.env.server.production` 和 `docker/.env.crawler.production`,不提交 Git。 + +## 首次部署 + +在源码已经准备到 `/www/wwwroot/sbnews` 后执行: + +```bash +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 +``` + +构建失败时不要删除现有容器和镜像;确认构建成功后再重新创建服务。Compose 不自动执行数据库迁移、SQL 导入或其他破坏性数据操作。 + +## 宝塔反向代理 + +宝塔负责三个站点的域名、证书和 HTTPS。各站点反向代理到本机回环地址,Docker 不占用宿主机 80/443。 + +### api.sbnews.net + +```nginx +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; +} +``` + +### keislwo.sbnews.net + +```nginx +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; +} +``` + +### web.sbnews.net + +```nginx +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; +} +``` + +在宝塔保存反代配置后执行 Nginx 配置检查并重载服务。不要把 `127.0.0.1:9000`、MySQL 或 Redis 直接配置为公网站点入口。 + +## 验收 + +```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/ +``` + +重点确认: + +- `server`、`scheduler`、`crawler`、`kb-worker`、`api-nginx`、`admin`、`uniapp-h5` 均处于运行状态。 +- Scheduler 能产生正常定时任务日志。 +- Crawler 容器已生成 `/etc/cron.d/sport-era-crawler`。 +- KB Worker 能连接 Redis Stream `ai:kb:sync:stream`,没有持续重试错误。 +- Admin 和 H5 刷新子路由不会返回 404。 +- Git 跟踪文件和部署文档中没有真实数据库、Redis 或 AI 密钥。 + +查看日志: + +```bash +sudo docker compose -f docker/docker-compose.prod.yml logs --tail 100 server api-nginx scheduler crawler kb-worker admin uniapp-h5 +``` + +## 回滚 + +确认当前工作区没有人工修改后,切回更新前保留的稳定标签: + +```bash +cd /www/wwwroot/sbnews +git status --short +git switch --detach production-stable +sudo docker compose -f docker/docker-compose.prod.yml up -d --build +``` + +回滚完成后,如需继续跟踪生产分支: + +```bash +git switch dev.1.0.0 +``` + +如果生产密码已经在聊天、日志或其他非私密位置暴露,部署完成后应立即轮换密码,并同步更新两份私有环境文件。