# SmartClean 本地模拟自动化打包部署方案 ## 一、目标 在本机用 Docker 模拟真实服务器环境,实现 **一键构建 → 镜像打包 → 部署 → 健康检查 → 回滚 → 飞书通知** 全流程自动化,为后续迁移到远程服务器 / CI/CD 打基础。 ## 二、整体架构 ``` 开发者本机 │ ├── 源码 (smartclean/) │ ├── deploy.sh 一键触发 │ ├── 1. Maven/npm 构建产物 │ ├── 2. Docker 镜像打包(带版本 tag) │ ├── 3. Docker Compose 部署(替换容器) │ ├── 4. 健康检查(轮询接口) │ ├── 5. 失败自动回滚(切回上一版本镜像) │ └── 6. 飞书 Webhook 通知结果 │ └── Docker 容器组(模拟生产服务器) ┌──────────────────────────────────────────────┐ │ smartclean-front (Nginx, 端口 80) │ │ smartclean-web (Tomcat 8.5, 端口 18095) │ │ smartclean-task (Spring Boot, 端口 18097) │ │ smartclean-mysql (MySQL 5.7, 端口 3307) │ │ smartclean-redis (Redis 6, 端口 6380) │ └──────────────────────────────────────────────┘ ``` > 端口刻意与本机开发环境错开,两套环境完全独立、互不干扰。 ## 三、新增文件结构 ``` smartclean/ └── deploy/ ├── docker-compose.yml # 服务编排定义 ├── .env # 环境变量(密码等敏感信息,不入 git) │ ├── Dockerfile.web # Web 服务镜像(Tomcat + ROOT.war) ├── Dockerfile.task # Task 服务镜像(JRE + JAR) ├── Dockerfile.front # 前端镜像(Nginx + 静态文件) │ ├── conf/ │ ├── nginx.conf # Nginx 反代配置(前端 → 后端) │ ├── application-docker.yml # Web 服务 Docker 专用 Spring 配置 │ └── application-task-docker.yml # Task 服务 Docker 专用 Spring 配置 │ ├── init-sql/ │ └── init.sh # 首次启动时自动建库导数据 │ ├── deploy.sh # 一键部署脚本(主入口) └── rollback.sh # 一键回滚脚本 ``` ## 四、各组件详细设计 ### 4.1 Docker Compose 编排 ```yaml # deploy/docker-compose.yml version: '3.8' services: # ==================== 基础设施 ==================== mysql: image: mysql:5.7 container_name: smartclean-mysql ports: - "3307:3306" environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} volumes: - mysql_data:/var/lib/mysql - ../sql:/docker-entrypoint-initdb.d # 首次启动自动导入 SQL command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s retries: 5 networks: - smartclean redis: image: redis:6-alpine container_name: smartclean-redis ports: - "6380:6379" command: redis-server --requirepass ${REDIS_PASSWORD} volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 10s retries: 3 networks: - smartclean # ==================== 应用服务 ==================== web: image: smartclean-web:${VERSION:-latest} container_name: smartclean-web build: context: .. dockerfile: deploy/Dockerfile.web ports: - "18095:8095" depends_on: mysql: condition: service_healthy redis: condition: service_healthy environment: SPRING_PROFILES_ACTIVE: docker volumes: - ./conf/application-docker.yml:/app/config/application-docker.yml - web_logs:/app/logs healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:8095/dropDown/districtTree"] interval: 15s timeout: 5s retries: 5 start_period: 60s restart: on-failure:3 networks: - smartclean task: image: smartclean-task:${VERSION:-latest} container_name: smartclean-task build: context: .. dockerfile: deploy/Dockerfile.task ports: - "18097:8097" depends_on: mysql: condition: service_healthy redis: condition: service_healthy environment: SPRING_PROFILES_ACTIVE: docker volumes: - ./conf/application-task-docker.yml:/app/config/application-task-docker.yml - task_logs:/app/logs restart: on-failure:3 networks: - smartclean frontend: image: smartclean-front:${VERSION:-latest} container_name: smartclean-front build: context: .. dockerfile: deploy/Dockerfile.front ports: - "80:80" depends_on: - web volumes: - ./conf/nginx.conf:/etc/nginx/conf.d/default.conf healthcheck: test: ["CMD", "curl", "-sf", "http://localhost/"] interval: 10s retries: 3 restart: on-failure:3 networks: - smartclean volumes: mysql_data: redis_data: web_logs: task_logs: networks: smartclean: driver: bridge ``` ### 4.2 Dockerfile #### Web 服务(Tomcat + WAR) ```dockerfile # deploy/Dockerfile.web FROM maven:3.8-openjdk-8 AS builder WORKDIR /src COPY backend/xiaoqu-intellectual-public/ xiaoqu-intellectual-public/ COPY backend/xiaoqu-intellectual-web/ xiaoqu-intellectual-web/ RUN cd xiaoqu-intellectual-public && mvn clean install -q -DskipTests \ && cd ../xiaoqu-intellectual-web && mvn clean package -q -DskipTests FROM tomcat:8.5-jdk8-temurin RUN rm -rf /usr/local/tomcat/webapps/* COPY --from=builder /src/xiaoqu-intellectual-web/target/ROOT.war /usr/local/tomcat/webapps/ # 注入外部配置目录 ENV SPRING_CONFIG_ADDITIONAL_LOCATION=/app/config/ RUN mkdir -p /app/config /app/logs EXPOSE 8095 ``` #### Task 服务(Spring Boot JAR) ```dockerfile # deploy/Dockerfile.task FROM maven:3.8-openjdk-8 AS builder WORKDIR /src COPY backend/xiaoqu-intellectual-public/ xiaoqu-intellectual-public/ COPY backend/xiaoqu-intellectual-task/ xiaoqu-intellectual-task/ RUN cd xiaoqu-intellectual-public && mvn clean install -q -DskipTests \ && cd ../xiaoqu-intellectual-task && mvn clean package -q -DskipTests FROM openjdk:8-jre-slim WORKDIR /app RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* COPY --from=builder /src/xiaoqu-intellectual-task/target/*.jar app.jar RUN mkdir -p /app/config /app/logs EXPOSE 8097 ENTRYPOINT ["java", "-jar", "app.jar", \ "--spring.config.additional-location=/app/config/"] ``` #### 前端(多阶段构建 → Nginx) ```dockerfile # deploy/Dockerfile.front FROM node:16-alpine AS builder WORKDIR /app COPY frontend/witcleansystem/package*.json ./ RUN npm ci --registry=https://registry.npmmirror.com COPY frontend/witcleansystem/ ./ RUN npm run build FROM nginx:1.24-alpine RUN rm -rf /usr/share/nginx/html/* COPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 ``` ### 4.3 Nginx 反代配置 ```nginx # deploy/conf/nginx.conf server { listen 80; server_name localhost; # 前端静态文件 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; # SPA history fallback } # API 反代到后端 Web 服务 location /api/ { proxy_pass http://web:8095/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { root /usr/share/nginx/html; expires 7d; add_header Cache-Control "public, immutable"; } } ``` ### 4.4 Docker 专用 Spring 配置 ```yaml # deploy/conf/application-docker.yml # 覆盖 application-test.yml 中的远程地址,指向 Docker 容器 server: port: 8095 redis: host: redis # Docker 容器名 port: 6379 password: ${REDIS_PASSWORD:kaixinjiuhao} spring: datasource: db1: url: jdbc:mysql://mysql:3306/xiaoqu_comples_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true username: root password: ${DB_PASSWORD:kaixinjiuhao} db2: url: jdbc:mysql://mysql:3306/xiaoqu_intellectual_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true username: root password: ${DB_PASSWORD:kaixinjiuhao} xxl: job: admin: addresses: http://xxl-job-admin:8080/xxl-job-admin # 可选,本地模拟可留空 ``` ### 4.5 环境变量文件 ```bash # deploy/.env(不入 git) DB_PASSWORD=kaixinjiuhao REDIS_PASSWORD=kaixinjiuhao VERSION=latest FEISHU_WEBHOOK=https://open.feishu.cn/open-apis/bot/v2/hook/5703e8cc-6998-46a6-af9d-8c5102cc8c1e ``` ## 五、一键部署脚本 ```bash # deploy/deploy.sh #!/bin/bash # # SmartClean 一键部署脚本(本地 Docker 模拟) # # 用法: # ./deploy.sh # 部署所有服务 # ./deploy.sh web # 仅重建部署 Web 服务 # ./deploy.sh task # 仅重建部署 Task 服务 # ./deploy.sh front # 仅重建部署前端 # ./deploy.sh rollback # 回滚到上一版本 set -e DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT_DIR="$(dirname "$DEPLOY_DIR")" source "$DEPLOY_DIR/.env" # ===== 版本号:日期-git短hash ===== GIT_HASH=$(cd "$ROOT_DIR" && git rev-parse --short HEAD) VERSION="v$(date +%Y%m%d)-${GIT_HASH}" BACKUP_FILE="$DEPLOY_DIR/.last-version" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } # ===== 飞书通知 ===== notify_feishu() { local title="$1" content="$2" color="$3" curl -s -X POST "$FEISHU_WEBHOOK" \ -H "Content-Type: application/json" \ -d "{ \"msg_type\": \"interactive\", \"card\": { \"header\": {\"title\":{\"tag\":\"plain_text\",\"content\":\"$title\"},\"template\":\"$color\"}, \"elements\": [{\"tag\":\"markdown\",\"content\":\"$content\"}] } }" > /dev/null 2>&1 } # ===== 备份当前版本号 ===== backup_version() { if [ -f "$DEPLOY_DIR/.current-version" ]; then cp "$DEPLOY_DIR/.current-version" "$BACKUP_FILE" log_info "已备份当前版本: $(cat "$BACKUP_FILE")" fi } # ===== 构建镜像 ===== build_images() { local target="${1:-all}" local start_time=$(date +%s) cd "$DEPLOY_DIR" case "$target" in web) log_info "构建 Web 镜像 ($VERSION)..." docker compose build web ;; task) log_info "构建 Task 镜像 ($VERSION)..." docker compose build task ;; front) log_info "构建前端镜像 ($VERSION)..." docker compose build frontend ;; all) log_info "构建全部镜像 ($VERSION)..." docker compose build web task frontend ;; esac local elapsed=$(( $(date +%s) - start_time )) log_info "镜像构建完成 (${elapsed}s)" } # ===== 部署服务 ===== deploy_services() { local target="${1:-all}" cd "$DEPLOY_DIR" export VERSION # 确保基础设施先启动 log_info "确保 MySQL + Redis 运行中..." docker compose up -d mysql redis log_info "等待数据库就绪..." docker compose exec mysql mysqladmin ping -h localhost --wait=30 --silent 2>/dev/null case "$target" in web) docker compose up -d --no-deps web ;; task) docker compose up -d --no-deps task ;; front) docker compose up -d --no-deps frontend ;; all) docker compose up -d web task frontend ;; esac log_info "容器已启动,开始健康检查..." } # ===== 健康检查 ===== healthcheck() { local target="${1:-all}" local max_retries=20 local interval=5 # 检查 Web 服务 if [ "$target" = "all" ] || [ "$target" = "web" ]; then log_info "检查 Web 服务..." for i in $(seq 1 $max_retries); do if curl -sf http://localhost:18095/dropDown/districtTree > /dev/null 2>&1; then log_info "✅ Web 服务健康 (第${i}次检查)" break fi if [ $i -eq $max_retries ]; then log_error "❌ Web 服务健康检查失败" return 1 fi sleep $interval done fi # 检查前端 if [ "$target" = "all" ] || [ "$target" = "front" ]; then log_info "检查前端服务..." for i in $(seq 1 $max_retries); do if curl -sf http://localhost:80/ > /dev/null 2>&1; then log_info "✅ 前端服务健康 (第${i}次检查)" break fi if [ $i -eq $max_retries ]; then log_error "❌ 前端服务健康检查失败" return 1 fi sleep $interval done fi return 0 } # ===== 回滚 ===== rollback() { if [ ! -f "$BACKUP_FILE" ]; then log_error "没有可回滚的版本" exit 1 fi local old_version=$(cat "$BACKUP_FILE") log_warn "回滚到版本: $old_version" export VERSION="$old_version" cd "$DEPLOY_DIR" docker compose up -d web task frontend echo "$old_version" > "$DEPLOY_DIR/.current-version" log_info "回滚完成" notify_feishu "⚠️ SmartClean 已回滚" \ "**回滚版本:** $old_version\\n**触发原因:** 健康检查失败" \ "yellow" } # ===== 主流程 ===== TARGET="${1:-all}" DEPLOY_START=$(date +%s) BRANCH=$(cd "$ROOT_DIR" && git rev-parse --abbrev-ref HEAD) COMMIT=$(cd "$ROOT_DIR" && git log -1 --format='%h %s') if [ "$TARGET" = "rollback" ]; then rollback exit 0 fi log_info "==============================" log_info " SmartClean 自动化部署" log_info " 版本: $VERSION" log_info " 分支: $BRANCH" log_info " 目标: $TARGET" log_info "==============================" # 1. 备份当前版本 backup_version # 2. 构建镜像 build_images "$TARGET" # 3. 部署 deploy_services "$TARGET" # 4. 健康检查 if healthcheck "$TARGET"; then # 记录当前版本 echo "$VERSION" > "$DEPLOY_DIR/.current-version" ELAPSED=$(( $(date +%s) - DEPLOY_START )) log_info "==============================" log_info " ✅ 部署成功!耗时 ${ELAPSED}s" log_info " 前端: http://localhost" log_info " Web: http://localhost:18095" log_info " Task: http://localhost:18097" log_info "==============================" notify_feishu "✅ SmartClean 部署成功" \ "**版本:** $VERSION\\n**分支:** $BRANCH\\n**提交:** $COMMIT\\n**目标:** $TARGET\\n**耗时:** ${ELAPSED}s" \ "green" else log_error "健康检查失败,自动回滚..." rollback ELAPSED=$(( $(date +%s) - DEPLOY_START )) notify_feishu "❌ SmartClean 部署失败(已回滚)" \ "**版本:** $VERSION\\n**分支:** $BRANCH\\n**提交:** $COMMIT\\n**耗时:** ${ELAPSED}s\\n**状态:** 已自动回滚" \ "red" exit 1 fi ``` ## 六、回滚脚本 ```bash # deploy/rollback.sh #!/bin/bash # 快捷回滚入口 DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)" exec "$DEPLOY_DIR/deploy.sh" rollback ``` ## 七、使用方式 ### 首次启动 ```bash cd deploy/ # 1. 创建 .env 文件(设置密码等) cp .env.example .env # 2. 一键构建 + 部署全部服务 ./deploy.sh ``` ### 日常使用 ```bash # 修改了后端代码,只重建部署 Web 服务 ./deploy.sh web # 修改了前端代码,只重建部署前端 ./deploy.sh front # 全量部署 ./deploy.sh # 出问题了,一键回滚 ./deploy.sh rollback # 或 ./rollback.sh ``` ### 查看日志 ```bash # 查看所有服务日志 docker compose -f deploy/docker-compose.yml logs -f # 只看 Web 服务 docker compose -f deploy/docker-compose.yml logs -f web # 查看容器状态 docker compose -f deploy/docker-compose.yml ps ``` ### 停止 / 清理 ```bash # 停止所有容器(数据保留) docker compose -f deploy/docker-compose.yml down # 停止并删除数据卷(慎用,会丢数据库数据) docker compose -f deploy/docker-compose.yml down -v ``` ## 八、端口映射总览 | 服务 | 容器内端口 | 宿主机端口 | 用途 | |------|-----------|-----------|------| | 前端 (Nginx) | 80 | **80** | 浏览器访问入口 | | Web 后端 | 8095 | **18095** | API 接口(也可通过 Nginx `/api` 访问) | | Task 后端 | 8097 | **18097** | 定时任务服务 | | MySQL | 3306 | **3307** | 本机连接调试用 | | Redis | 6379 | **6380** | 本机连接调试用 | > 与本机开发环境 (8079/8095/3306/6379) 完全隔离。 ## 九、与本机开发环境的对比 ``` ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ 本机开发环境(已有) │ │ Docker 模拟部署(新增) │ │ │ │ │ │ npm run dev → :8079 │ │ Nginx → :80 │ │ mvn spring-boot:run → :8095│ │ Tomcat 容器 → :18095 │ │ 本机 MySQL → :3306 │ │ MySQL 容器 → :3307 │ │ 本机 Redis → :6379 │ │ Redis 容器 → :6380 │ │ │ │ │ │ 用途: 开发调试、热重载 │ │ 用途: 模拟生产部署流程 │ └─────────────────────────────┘ └─────────────────────────────┘ ``` ## 十、后续演进路径 本方案在本机验证通过后,可以低成本迁移到真实环境: | 阶段 | 动作 | 改动量 | |------|------|--------| | **当前** | 本机 Docker 模拟 | 本方案 | | **阶段 2** | 远程服务器部署 | deploy.sh 加 SSH 远程执行,或把镜像 push 到私有 Registry | | **阶段 3** | Gitea Actions CI/CD | 把 deploy.sh 的逻辑搬到 `.gitea/workflows/deploy.yml`,push 自动触发 | | **阶段 4** | 多环境 | docker-compose.prod.yml 覆盖生产配置,同一套镜像部署到不同环境 | 每个阶段都是增量改动,核心的 Dockerfile 和 docker-compose.yml 不需要重写。