Files
sbnews/docs/superpowers/plans/2026-08-02-production-docker-compose.md
T

1009 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 完整构建,不自动连接或修改生产数据库,也不自动操作宝塔配置。