这下发布不需要Jenkins了

智能增量部署方案

基于 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

最佳实践

  1. 日志保留 - 部署日志保留 7 天,定期清理
  2. 健康检查 - 部署后验证 HTTP 状态码
  3. 失败告警 - 部署失败立即邮件通知
  4. 版本标签 - 镜像打上 git commit hash 标签,方便回滚
  5. 分支策略 - 只有 master/main 分支触发部署

扩展方向

  • 添加 Slack/钉钉 通知
  • 支持蓝绿部署
  • 添加自动回滚机制
  • 集成监控告警(Prometheus)
  • 支持多环境(dev/staging/prod)

文档版本: v1.0 | 最后更新: 2025-12-29

相关推荐
回忆是昨天里的海2 小时前
docker自定义网络-简单总结
运维·docker·容器
小鹏linux2 小时前
【linux】进程与服务管理命令 - at
linux·运维·服务器
0和1的舞者2 小时前
Git 实战踩坑:如何让多个 IDE 项目共用一个远程仓库(附子模块问题解决)
git·开发·仓库·码云·子模块·操作·冲突
博语小屋2 小时前
TCP:协议、序列化与反序列化、JSON 数据和jsoncpp
linux·网络·网络协议·tcp/ip·json
硬核子牙2 小时前
手写生产级eBPF内存检测工具
linux
.生产的驴2 小时前
DockerCompoe 部署注册中心Nacos 一键部署 单机+Mysql8
java·linux·运维·spring boot·缓存·docker·doc
FIT2CLOUD飞致云2 小时前
操作教程丨通过1Panel轻松安装和管理MySQL开源数据库
linux·运维·服务器·mysql·开源·1panel
全栈游侠2 小时前
GT2933触摸驱动分析 -中断处理
linux·笔记
QT 小鲜肉3 小时前
【Linux命令大全】001.文件管理之lsattr命令(实操篇)
linux·运维·服务器·笔记·elasticsearch