- 新增 deploy/docker/:Docker 本机模拟部署,含 Dockerfile、docker-compose、deploy.sh 一键脚本 - 新增 deploy/remote/:远程服务器部署,含 SSH 自动上传、重启、回滚脚本 - 新增 deploy/README.md:完整使用手册,含现状分析、落地调整工作清单、命令速查 - 新增 build.sh/start.sh:本地构建和启动脚本(含飞书通知) - 新增前端 .env.docker 环境配置,API 指向测试服务器 - 前端 package.json 新增 build-docker 命令 - 更新 .gitignore:排除 IDE 配置、SQL 数据、Docker 敏感文件 - 前端 UI 样式优化(多个页面组件) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
19 KiB
19 KiB
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 编排
# 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)
# 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)
# 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)
# 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 反代配置
# 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 配置
# 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 环境变量文件
# 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
五、一键部署脚本
# 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
六、回滚脚本
# deploy/rollback.sh
#!/bin/bash
# 快捷回滚入口
DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)"
exec "$DEPLOY_DIR/deploy.sh" rollback
七、使用方式
首次启动
cd deploy/
# 1. 创建 .env 文件(设置密码等)
cp .env.example .env
# 2. 一键构建 + 部署全部服务
./deploy.sh
日常使用
# 修改了后端代码,只重建部署 Web 服务
./deploy.sh web
# 修改了前端代码,只重建部署前端
./deploy.sh front
# 全量部署
./deploy.sh
# 出问题了,一键回滚
./deploy.sh rollback
# 或
./rollback.sh
查看日志
# 查看所有服务日志
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
停止 / 清理
# 停止所有容器(数据保留)
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 不需要重写。