智能增量部署方案
基于 Git Diff 的轻量级 CI/CD 方案,适用于 Monorepo 前端项目
方案概述
核心特性
| 特性 | 说明 |
|---|---|
| 🎯 智能检测 | 根据 Git 提交自动识别变更模块,只部署需要更新的部分 |
| ⚡ 增量构建 | Docker 层缓存优化,依赖不变时秒级构建 |
| 📧 邮件通知 | 部署结果 + Commit 详情,团队实时掌握 |
| 🔄 多触发方式 | 支持定时任务、Git Webhook、手动触发 |
| 🛡️ 零停机 | 容器级别滚动更新,服务不中断 |
| 📦 零依赖 | 纯 Shell + Git,无需 Jenkins/GitHub Actions |
适用场景
- Monorepo 多应用项目(如 PC + Mobile + Admin)
- 中小团队快速落地 CI/CD
- 服务器资源有限,需要轻量方案
- 不想维护复杂流水线工具
性能对比
| 场景 | 传统全量部署 | 智能增量部署 |
|---|---|---|
| 只改 PC 代码 | 5 分钟 | 30 秒 |
| 改多个模块 | 5 分钟 | 1-2 分钟 |
| 依赖无变化 | 5 分钟 | 构建秒过 |
架构设计
整体流程
bash
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Git Push │────▶│ Webhook │────▶│ 智能检测 │
│ 定时任务 │ │ Server │ │ 变更模块 │
│ 手动触发 │ │ │ │ │
└─────────────┘ └─────────────┘ └──────┬──────┘
│
┌──────────────────────────┼──────────────────────────┐
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Git Diff 分析 │ │
│ │ packages/pc/* → 部署 PC │ │
│ │ packages/mobile/* → 部署 Mobile │ │
│ │ packages/admin/* → 部署 Admin │ │
│ │ package.json/docker/* → 全量部署 │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
└──────────────────────────┼──────────────────────────┘
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 构建镜像 │────▶│ 重启容器 │────▶│ 邮件通知 │
│ (增量缓存) │ │ (滚动更新) │ │ (结果+日志)│
└─────────────┘ └─────────────┘ └─────────────┘
容器架构
java
┌─────────────────────────────┐
│ Edge Nginx (网关) │
│ 端口: 8085/8445 │
└──────────────┬──────────────┘
│
┌────────────────────────────┼────────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ frontend-pc │ │frontend-mobile│ │frontend-admin │
│ (Nginx) │ │ (Nginx) │ │ (Nginx) │
└───────────────┘ └───────────────┘ └───────────────┘
快速开始
前置要求
- Linux 服务器(CentOS/Ubuntu)
- Docker 或 Podman
- Git
- Node.js 项目(pnpm/npm/yarn)
目录结构
perl
your-project/
├── packages/
│ ├── pc/ # PC 端应用
│ ├── mobile/ # 移动端应用
│ └── admin/ # 管理后台
├── docker/
│ ├── frontend.Dockerfile
│ └── nginx/
│ ├── edge.conf # 网关配置
│ └── spa.conf # SPA 配置
├── scripts/
│ ├── deploy.sh # 主部署脚本
│ ├── deploy-docker-wrapper.sh # 智能部署包装器
│ ├── send-mail.py # 邮件通知
│ └── webhook-server.py # Webhook 服务
├── docker-compose.yml
└── package.json
详细配置
1. Dockerfile 优化(关键!)
dockerfile
# docker/frontend.Dockerfile
FROM node:20-bullseye AS builder
WORKDIR /app
# 启用 pnpm
ENV PNPM_HOME="/root/.local/share/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN pnpm config set registry https://registry.npmmirror.com
ARG APP_NAME
ARG PACKAGE_NAME
# ========== 缓存优化核心 ==========
# 1. 先复制依赖定义文件(很少变化)
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
COPY packages/pc/package.json packages/pc/package.json
COPY packages/mobile/package.json packages/mobile/package.json
COPY packages/admin/package.json packages/admin/package.json
# 2. 安装依赖(只有 package.json 变化才重新执行)
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
# 3. 复制源代码(经常变化,但不影响上面的缓存)
COPY packages/${APP_NAME}/src packages/${APP_NAME}/src
COPY packages/${APP_NAME}/public packages/${APP_NAME}/public
COPY packages/${APP_NAME}/index.html packages/${APP_NAME}/index.html
COPY packages/${APP_NAME}/vite.config.ts packages/${APP_NAME}/vite.config.ts
COPY packages/${APP_NAME}/tsconfig*.json packages/${APP_NAME}/
# 4. 构建
RUN pnpm --filter ${PACKAGE_NAME} build
# 运行阶段
FROM nginx:1.27-alpine
ARG APP_NAME
COPY docker/nginx/spa.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/packages/${APP_NAME}/dist /usr/share/nginx/html
EXPOSE 80
原理说明:
- Docker 按层缓存,只有某层的输入变化才会重新执行
- 把
package.json和源代码分开复制 - 依赖安装在源代码复制之前,这样改代码不会触发重新安装依赖
2. 智能检测脚本
bash
#!/bin/bash
# scripts/deploy-docker-wrapper.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="${SCRIPT_DIR}/.."
cd "$PROJECT_DIR"
# 记录拉取前的 commit
BEFORE=$(git rev-parse HEAD)
# 拉取最新代码
git pull
# 记录拉取后的 commit
AFTER=$(git rev-parse HEAD)
# ========== 智能检测核心 ==========
DEPLOY_ARGS=""
if [ "$BEFORE" != "$AFTER" ]; then
# 获取变更文件列表
CHANGED_FILES=$(git diff --name-only "$BEFORE" "$AFTER")
# 检测各模块变更
if echo "$CHANGED_FILES" | grep -q "^packages/pc/"; then
DEPLOY_ARGS="$DEPLOY_ARGS --frontend-pc"
echo "📦 检测到 PC 端变更"
fi
if echo "$CHANGED_FILES" | grep -q "^packages/mobile/"; then
DEPLOY_ARGS="$DEPLOY_ARGS --frontend-mobile"
echo "📦 检测到 Mobile 端变更"
fi
if echo "$CHANGED_FILES" | grep -q "^packages/admin/"; then
DEPLOY_ARGS="$DEPLOY_ARGS --frontend-admin"
echo "📦 检测到 Admin 端变更"
fi
# 公共文件变更则全量部署
if echo "$CHANGED_FILES" | grep -qE "^(package\.json|pnpm-lock|docker/|scripts/)"; then
DEPLOY_ARGS="--frontend"
echo "📦 检测到公共配置变更,全量部署"
fi
fi
# 无变更时的默认行为
if [ -z "$DEPLOY_ARGS" ]; then
echo "📦 无变更检测,跳过部署"
exit 0
fi
# 执行部署
./scripts/deploy.sh $DEPLOY_ARGS
3. 主部署脚本
bash
#!/bin/bash
# scripts/deploy.sh
set -e
# 解析参数
DEPLOY_PC=false
DEPLOY_MOBILE=false
DEPLOY_ADMIN=false
for arg in "$@"; do
case $arg in
--frontend-pc) DEPLOY_PC=true ;;
--frontend-mobile) DEPLOY_MOBILE=true ;;
--frontend-admin) DEPLOY_ADMIN=true ;;
--frontend) DEPLOY_PC=true; DEPLOY_MOBILE=true; DEPLOY_ADMIN=true ;;
esac
done
# 构建镜像
if [ "$DEPLOY_PC" = true ]; then
echo "🔨 构建 PC 端..."
docker compose build frontend-pc
fi
if [ "$DEPLOY_MOBILE" = true ]; then
echo "🔨 构建 Mobile 端..."
docker compose build frontend-mobile
fi
if [ "$DEPLOY_ADMIN" = true ]; then
echo "🔨 构建 Admin 端..."
docker compose build frontend-admin
fi
# 重启容器(增量模式,只重启变更的)
if [ "$DEPLOY_PC" = true ]; then
docker stop frontend-pc 2>/dev/null || true
docker rm -f frontend-pc 2>/dev/null || true
docker run -d --name frontend-pc --network app-net frontend-pc:latest
fi
# ... 其他容器类似
# 重载网关配置
docker exec edge-nginx nginx -s reload
echo "✅ 部署完成"
4. docker-compose.yml
yaml
version: '3.8'
services:
edge:
image: nginx:1.27-alpine
container_name: edge-nginx
ports:
- "8085:80"
volumes:
- ./docker/nginx/edge.conf:/etc/nginx/nginx.conf:ro
networks:
- app-net
depends_on:
- frontend-pc
- frontend-mobile
- frontend-admin
frontend-pc:
build:
context: .
dockerfile: docker/frontend.Dockerfile
args:
APP_NAME: pc
PACKAGE_NAME: your-pc-package-name
container_name: frontend-pc
networks:
- app-net
frontend-mobile:
build:
context: .
dockerfile: docker/frontend.Dockerfile
args:
APP_NAME: mobile
PACKAGE_NAME: your-mobile-package-name
container_name: frontend-mobile
networks:
- app-net
frontend-admin:
build:
context: .
dockerfile: docker/frontend.Dockerfile
args:
APP_NAME: admin
PACKAGE_NAME: your-admin-package-name
container_name: frontend-admin
networks:
- app-net
networks:
app-net:
driver: bridge
5. Nginx 网关配置
nginx
# docker/nginx/edge.conf
events {
worker_connections 1024;
}
http {
upstream pc {
server frontend-pc:80;
}
upstream mobile {
server frontend-mobile:80;
}
upstream admin {
server frontend-admin:80;
}
server {
listen 80;
server_name www.example.com example.com;
location / {
proxy_pass http://pc;
}
}
server {
listen 80;
server_name m.example.com;
location / {
proxy_pass http://mobile;
}
}
server {
listen 80;
server_name admin.example.com;
location / {
proxy_pass http://admin;
}
}
}
6. SPA Nginx 配置
nginx
# docker/nginx/spa.conf
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
# Gzip 压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
# 静态资源缓存
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA 路由
location / {
try_files $uri $uri/ /index.html;
}
}
触发方式
方式一:定时任务(Cron)
bash
# 编辑 crontab
crontab -e
# 每 3 小时执行一次
0 */3 * * * /path/to/project/scripts/deploy-docker-wrapper.sh >> /var/log/deploy.log 2>&1
方式二:Git Webhook
python
# scripts/webhook-server.py
from http.server import HTTPServer, BaseHTTPRequestHandler
import subprocess
import json
class WebhookHandler(BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
# 验证签名(可选)
# ...
# 触发部署
subprocess.Popen(['/path/to/scripts/deploy-docker-wrapper.sh'])
self.send_response(200)
self.end_headers()
self.wfile.write(b'OK')
if __name__ == '__main__':
server = HTTPServer(('0.0.0.0', 9000), WebhookHandler)
print('Webhook server running on port 9000')
server.serve_forever()
启动 Webhook 服务:
bash
nohup python3 scripts/webhook-server.py > /var/log/webhook.log 2>&1 &
在 Git 仓库设置 Webhook URL:http://your-server:9000/
方式三:手动触发
bash
# 全量部署
./scripts/deploy.sh --frontend
# 只部署 PC
./scripts/deploy.sh --frontend-pc
# 智能检测部署
./scripts/deploy-docker-wrapper.sh
邮件通知(可选)
python
# scripts/send-mail.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import sys
SMTP_SERVER = "smtp.qq.com"
SMTP_PORT = 465
SENDER = "your-email@qq.com"
PASSWORD = "your-smtp-password" # QQ邮箱使用授权码
RECEIVERS = ["team@example.com"]
def send_mail(subject, body, html_body=None):
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = SENDER
msg['To'] = ', '.join(RECEIVERS)
msg.attach(MIMEText(body, 'plain', 'utf-8'))
if html_body:
msg.attach(MIMEText(html_body, 'html', 'utf-8'))
with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as server:
server.login(SENDER, PASSWORD)
server.sendmail(SENDER, RECEIVERS, msg.as_string())
print("邮件发送成功")
if __name__ == '__main__':
subject = sys.argv[1] if len(sys.argv) > 1 else "部署通知"
body = sys.argv[2] if len(sys.argv) > 2 else ""
html_body = sys.argv[3] if len(sys.argv) > 3 else None
send_mail(subject, body, html_body)
常见问题
Q: 容器名称冲突怎么办?
使用 docker rm -f 强制删除,或者用 --replace 参数(Podman):
bash
docker rm -f container-name 2>/dev/null || true
docker run -d --name container-name ...
Q: 网关无法识别新容器?
重启容器后需要 reload nginx:
bash
docker exec edge-nginx nginx -s reload
Q: 如何回滚?
bash
# 查看历史镜像
docker images | grep frontend-pc
# 使用指定版本启动
docker run -d --name frontend-pc frontend-pc:v1.0.0
Q: 如何查看部署日志?
bash
# 查看最新日志
tail -f /path/to/logs/deploy-*.log
# 查看容器日志
docker logs frontend-pc --tail 50
最佳实践
- 日志保留 - 部署日志保留 7 天,定期清理
- 健康检查 - 部署后验证 HTTP 状态码
- 失败告警 - 部署失败立即邮件通知
- 版本标签 - 镜像打上 git commit hash 标签,方便回滚
- 分支策略 - 只有 master/main 分支触发部署
扩展方向
- 添加 Slack/钉钉 通知
- 支持蓝绿部署
- 添加自动回滚机制
- 集成监控告警(Prometheus)
- 支持多环境(dev/staging/prod)
文档版本: v1.0 | 最后更新: 2025-12-29