现代全栈项目实战:分支管理、Vercel 部署、Docker 容器化与 CI/CD 完整指南
基于真实 Monorepo 项目的最佳实践总结
📋 目录
- [Git 分支管理策略](#Git 分支管理策略)
- [Vercel 前端部署详解](#Vercel 前端部署详解)
- [Docker 容器化部署](#Docker 容器化部署)
- [CI/CD 流水线配置](#CI/CD 流水线配置)
- 常见问题与解决方案
1. Git 分支管理策略
1.1 主流分支管理方案对比
Git Flow(经典方案)
main (生产环境)
├── develop (开发主干)
│ ├── feature/user-auth (功能分支)
│ ├── feature/blog-editor (功能分支)
│ └── hotfix/security-patch (紧急修复)
└── release/v1.0.0 (发布分支)
特点:
- ✅ 结构清晰,适合大型团队
- ❌ 流程复杂,学习成本高
- 🎯 适用场景:传统企业级应用、多版本并行开发
GitHub Flow(简化方案)
main (唯一长期分支)
├── feature/add-search (功能分支)
├── fix/cors-issue (修复分支)
└── docs/update-readme (文档分支)
特点:
- ✅ 简单直观,快速迭代
- ❌ 缺少明确的发布流程
- 🎯 适用场景:SaaS 应用、持续部署项目
Trunk Based Development(主干开发)
main (主干)
├── short-lived-feature-1 (短期分支 < 1天)
├── short-lived-feature-2 (短期分支 < 1天)
└── feature-flags (通过特性开关控制)
特点:
- ✅ 减少合并冲突,快速集成
- ❌ 需要完善的测试和特性开关机制
- 🎯 适用场景:高成熟度团队、超大型项目
1.2 推荐方案:混合分支策略(本项目采用)
基于实际项目经验,我们采用改进的 GitHub Flow :

分支命名规范
| 分支类型 | 命名格式 | 示例 | 说明 |
|---|---|---|---|
| 主分支 | main |
main |
生产环境代码 |
| 开发分支 | develop |
develop |
集成测试环境 |
| 功能分支 | feature/* |
feature/user-auth |
新功能开发 |
| 修复分支 | fix/* |
fix/cors-issue |
Bug 修复 |
| 文档分支 | docs/* |
docs/update-readme |
文档更新 |
| 热修复 | hotfix/* |
hotfix/security-patch |
线上紧急修复 |
| 发布分支 | release/* |
release/v1.0.0 |
版本发布准备 |
1.3 分支操作流程详解
场景一:新功能开发流程
bash
# 1. 从 main 创建功能分支
git checkout main
git pull origin main
git checkout -b feature/new-feature
# 2. 开发并提交代码
git add .
git commit -m "feat: 添加新功能模块"
# 3. 推送到远程
git push -u origin feature/new-feature
# 4. 创建 Pull Request (GitHub/GitLab)
# 在 Web 界面创建 PR,选择 base: main, compare: feature/new-feature
# 5. Code Review 通过后合并
# 使用 Squash Merge 保持提交历史整洁
# 6. 删除远程分支
git push origin --delete feature/new-feature
# 7. 删除本地分支
git branch -d feature/new-feature
场景二:线上 Bug 修复流程(Hotfix)
bash
# 1. 从 main 创建热修复分支
git checkout main
git pull origin main
git checkout -b hotfix/critical-bug
# 2. 修复 Bug
git add .
git commit -m "fix: 修复线上关键 Bug #123"
# 3. 推送到远程并创建 PR
git push -u origin hotfix/critical-bug
# 4. 紧急情况下可以直接合并到 main
git checkout main
git merge hotfix/critical-bug
git push origin main
# 5. 同时合并到 develop 保持一致性
git checkout develop
git merge hotfix/critical-bug
git push origin develop
# 6. 打上版本标签
git tag -a v1.0.1 -m "修复关键 Bug"
git push origin v1.0.1
场景三:处理分支冲突
bash
# 1. 切换到目标分支并更新
git checkout feature/my-feature
git fetch origin
git rebase origin/main
# 2. 如果有冲突,手动解决后继续
# 编辑冲突文件...
git add .
git rebase --continue
# 3. 如果冲突太复杂,可以中止 rebase
git rebase --abort
# 4. 强制推送(谨慎使用)
git push --force-with-lease
1.4 分支保护规则(GitHub Settings)
推荐配置
yaml
Branch Protection Rules for 'main':
✓ Require pull request reviews before merging
- Required approving reviews: 1
- Dismiss stale pull request approvals: ✓
- Require review from Code Owners: ✓
✓ Require status checks to pass before merging
- Required checks:
- "Test Suite"
- "Build and Deploy"
- Require branches to be up to date: ✓
✓ Include administrators: ✓
✓ Allow force pushes: ✗
✓ Allow deletions: ✗
✓ Require linear history: ✓ (禁止 Merge Commit)
1.5 线上问题应急处理流程
Critical
Normal
是
否
发现线上问题
严重程度?
创建 hotfix 分支
创建 fix 分支
快速修复
本地测试
测试通过?
直接合并到 main
立即部署
验证修复效果
同步到 develop
打版本标签
正常开发流程
Code Review
合并到 develop
等待下次发布
关键原则:
- Hotfix 优先:线上 Critical Bug 必须走 hotfix 流程
- 双向合并:hotfix 既要合并到 main,也要合并到 develop
- 版本标签:每次 hotfix 都要打标签,方便回滚
- 事后复盘:记录问题原因,避免重复发生
2. Vercel 前端部署详解
2.1 Vercel 部署步骤
步骤一:项目准备
bash
# 1. 安装 Vercel CLI
npm install -g vercel@latest
# 2. 登录 Vercel
vercel login
# 3. 初始化项目(在项目根目录执行)
cd apps/frontend
vercel init
步骤二:配置 vercel.json
json
{
"framework": "vite",
"buildCommand": "pnpm build",
"outputDirectory": "dist",
"installCommand": "pnpm install",
"rewrites": [
{
"source": "/api/(.*)",
"destination": "http://localhost:3000/api/$1"
},
{
"source": "/(.*)",
"destination": "/index.html"
}
],
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-XSS-Protection",
"value": "1; mode=block"
}
]
}
]
}
配置解析:
| 字段 | 作用 | 说明 |
|---|---|---|
framework |
指定框架 | Vercel 自动优化构建流程 |
buildCommand |
构建命令 | 执行 pnpm build 生成静态文件 |
outputDirectory |
输出目录 | Vite 默认输出到 dist |
installCommand |
安装依赖 | 使用 pnpm 安装 |
rewrites |
路由重写 | 处理 API 代理和 SPA 路由 |
headers |
安全头 | 增强安全性 |
2.2 单页面应用(SPA)路由配置
问题背景
React/Vue 等 SPA 应用在刷新页面时会出现 404 错误,因为服务器找不到对应的物理文件。
解决方案
json
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}
工作原理:
用户访问 /blog/article-1
↓
Vercel 查找 /blog/article-1.html(不存在)
↓
触发 rewrite 规则
↓
返回 /index.html
↓
React Router 接管路由,渲染对应组件
注意事项:
- ⚠️ 此规则必须放在最后,避免覆盖其他 rewrite 规则
- ⚠️ 静态资源(CSS/JS/图片)不受影响,因为它们有具体文件路径
- ⚠️ SEO 友好性需要考虑 SSR 或预渲染方案
2.3 跨域配置(CORS)
前端 Vercel 配置
Vercel 本身不需要特殊 CORS 配置,关键在于后端配置。
后端 Nest.js CORS 配置
typescript
// apps/backend/src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// CORS 配置
app.enableCors({
origin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:5173'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 3600,
});
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
环境变量配置
bash
# .env.production
CORS_ORIGIN=https://your-app.vercel.app,https://www.your-domain.com
关键点:
- ✅ 生产环境必须配置具体的域名,不要使用
* - ✅ 多个域名用逗号分隔
- ✅ 包含
credentials: true时,origin不能为* - ✅ 定期审查允许的域名列表,移除废弃域名
2.4 环境变量管理
方式一:Vercel Dashboard 配置
- 进入项目 → Settings → Environment Variables
- 添加变量(支持 Development/Preview/Production 环境)
- 重新部署生效
方式二:CLI 配置
bash
# 添加环境变量
vercel env add VITE_API_URL production
# 输入值:https://your-api-domain.com/api
# 拉取环境变量到本地
vercel env pull .env.production.local
# 列出所有环境变量
vercel env ls
方式三:CI/CD 中自动配置
yaml
# .github/workflows/ci-cd.yml
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=production --token="${{ secrets.VERCEL_TOKEN }}"
working-directory: ./apps/frontend
env:
VITE_API_URL: ${{ secrets.VITE_API_URL }}
⚠️ 常见问题:VITE_ 前缀变量
Vite 只暴露以 VITE_ 开头的环境变量给客户端代码。如果在 Vercel 中配置了引用 Secret 的变量,可能导致构建失败。
解决方案:
bash
# 在 CI/CD 中移除 problematic 变量
vercel env rm VITE_API_URL production --token="${{ secrets.VERCEL_TOKEN }}" --yes
然后在代码中使用 fallback:
typescript
// apps/frontend/src/config/api.ts
export const API_URL = import.meta.env.VITE_API_URL || 'https://default-api-domain.com/api';
2.5 自定义域名配置
bash
# 添加自定义域名
vercel domains add your-domain.com
# 配置 DNS 记录
# Type: CNAME
# Name: www
# Value: cname.vercel-dns.com
# Type: A
# Name: @
# Value: 76.76.21.21
3. Docker 容器化部署
3.1 Docker 架构设计
本项目采用 Monorepo + 微服务 架构:
Docker Network: ai-blog-network
Database Layer
Backend Container
Frontend Container
Load Balancer / Nginx
React App :80
Nest.js API :3000
MongoDB :27017
Redis :6379
用户浏览器
3.2 MongoDB 容器配置
docker-compose.yml 配置
yaml
mongodb:
image: mongo:7.0
container_name: ai-blog-mongodb
restart: unless-stopped
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password123
MONGO_INITDB_DATABASE: ai_blog
volumes:
- mongodb_data:/data/db
- ./docker/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
networks:
- ai-blog-network
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
interval: 10s
timeout: 5s
retries: 5
关键配置说明:
| 配置项 | 作用 | 生产建议 |
|---|---|---|
restart: unless-stopped |
容器异常退出时自动重启 | ✅ 必须配置 |
volumes |
数据持久化 | ✅ 必须挂载到宿主机 |
healthcheck |
健康检查 | ✅ 确保服务可用 |
mongo-init.js |
初始化脚本 | 可选,用于创建索引等 |
初始化脚本示例
javascript
// docker/mongo-init.js
db = db.getSiblingDB('ai_blog');
// 创建集合
db.createCollection('users');
db.createCollection('articles');
db.createCollection('comments');
// 创建索引
db.users.createIndex({ email: 1 }, { unique: true });
db.articles.createIndex({ slug: 1 }, { unique: true });
db.articles.createIndex({ createdAt: -1 });
// 创建初始管理员用户
db.users.insertOne({
username: 'admin',
email: 'admin@example.com',
password: '$2b$10$...', // bcrypt hash
role: 'admin',
createdAt: new Date(),
});
数据备份与恢复
bash
# 备份数据库
docker exec ai-blog-mongodb mongodump \
--username admin \
--password password123 \
--authenticationDatabase admin \
--db ai_blog \
--out /tmp/backup
# 复制备份文件到宿主机
docker cp ai-blog-mongodb:/tmp/backup ./backup-$(date +%Y%m%d)
# 恢复数据库
docker cp ./backup-20240101 ai-blog-mongodb:/tmp/backup
docker exec ai-blog-mongodb mongorestore \
--username admin \
--password password123 \
--authenticationDatabase admin \
--db ai_blog \
/tmp/backup/ai_blog
3.3 Redis 容器配置
docker-compose.yml 配置
yaml
redis:
image: redis:7-alpine
container_name: ai-blog-redis
restart: unless-stopped
ports:
- "6379:6379"
command: redis-server --requirepass redis_password123
volumes:
- redis_data:/data
networks:
- ai-blog-network
healthcheck:
test: ["CMD", "redis-cli", "-a", "redis_password123", "ping"]
interval: 10s
timeout: 5s
retries: 5
高级配置(持久化):
yaml
command: >
redis-server
--requirepass redis_password123
--appendonly yes
--appendfsync everysec
--maxmemory 256mb
--maxmemory-policy allkeys-lru
配置说明:
| 参数 | 作用 | 推荐值 |
|---|---|---|
--appendonly yes |
开启 AOF 持久化 | 生产必开 |
--appendfsync everysec |
AOF 同步策略 | everysec(平衡性能与安全) |
--maxmemory |
最大内存限制 | 根据服务器内存设置 |
--maxmemory-policy |
内存淘汰策略 | allkeys-lru(缓存场景) |
常用 Redis 操作
bash
# 进入 Redis CLI
docker exec -it ai-blog-redis redis-cli -a redis_password123
# 查看内存使用情况
INFO memory
# 查看键数量
DBSIZE
# 清空数据库(谨慎使用)
FLUSHDB
# 监控实时命令
MONITOR
3.4 后端服务构建与部署
多阶段 Dockerfile
dockerfile
# docker/Dockerfile.backend
FROM node:22-alpine AS builder
WORKDIR /app
# 安装 pnpm
RUN npm install -g pnpm@10.10.0
# 复制 monorepo 文件
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml turbo.json ./
COPY packages/types/package.json ./packages/types/
COPY apps/backend/package.json ./apps/backend/
# 安装依赖
RUN pnpm install --frozen-lockfile || pnpm install
# 复制源代码
COPY packages/types ./packages/types
COPY apps/backend ./apps/backend
# 构建 types 包
RUN cd packages/types && pnpm build
# 构建 backend
RUN cd apps/backend && pnpm build
# 生产阶段
FROM node:22-alpine
WORKDIR /app
# 安装 pnpm
RUN npm install -g pnpm@10.10.0
# 复制 package 文件
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY apps/backend/package.json ./apps/backend/
# 仅安装生产依赖
RUN pnpm install --prod --frozen-lockfile || pnpm install --prod
# 复制构建产物
COPY --from=builder /app/apps/backend/dist ./apps/backend/dist
COPY --from=builder /app/packages/types/dist ./packages/types/dist
EXPOSE 3000
WORKDIR /app/apps/backend
CMD ["node", "dist/main.js"]
多阶段构建优势:
- ✅ 最终镜像不包含 devDependencies
- ✅ 不包含源代码和 TypeScript 编译器
- ✅ 镜像体积减小 60-70%
构建与运行
bash
# 构建镜像
docker build -f docker/Dockerfile.backend -t ai-blog-backend:latest .
# 运行容器
docker run -d \
--name ai-blog-backend \
--network ai-blog-network \
-p 3000:3000 \
-e NODE_ENV=production \
-e MONGODB_URI=mongodb://admin:password@mongodb:27017/ai_blog?authSource=admin \
-e REDIS_URL=redis://:password@redis:6379 \
-e JWT_SECRET=your-secret-key \
-e OPENAI_API_KEY=sk-xxx \
ai-blog-backend:latest
# 查看日志
docker logs -f ai-blog-backend
# 进入容器调试
docker exec -it ai-blog-backend sh
3.5 前端服务构建与部署
Nginx + React Dockerfile
dockerfile
# docker/Dockerfile.frontend
FROM node:18-alpine AS builder
WORKDIR /app
# 安装 pnpm
RUN npm install -g pnpm@8.9.0
# 复制 monorepo 文件
COPY package.json pnpm-workspace.yaml turbo.json ./
COPY packages/types/package.json ./packages/types/
COPY apps/frontend/package.json ./apps/frontend/
# 安装依赖
RUN pnpm install --frozen-lockfile
# 复制源代码
COPY packages/types ./packages/types
COPY apps/frontend ./apps/frontend
# 构建 types 包
RUN cd packages/types && pnpm build
# 构建前端
RUN cd apps/frontend && pnpm build
# 生产阶段:Nginx
FROM nginx:alpine
# 复制构建产物
COPY --from=builder /app/apps/frontend/dist /usr/share/nginx/html
# 复制 Nginx 配置
COPY apps/frontend/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Nginx 配置(SPA 路由支持)
nginx
# apps/frontend/nginx.conf
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip 压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_min_length 1000;
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API 代理到后端
location /api/ {
proxy_pass http://backend:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# SPA 路由回退
location / {
try_files $uri $uri/ /index.html;
}
# 健康检查
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
3.6 Docker Compose 完整配置
开发环境
yaml
# docker/docker-compose.yml
version: '3.8'
services:
mongodb:
image: mongo:7.0
container_name: ai-blog-mongodb
restart: unless-stopped
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password123
MONGO_INITDB_DATABASE: ai_blog
volumes:
- mongodb_data:/data/db
networks:
- ai-blog-network
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: ai-blog-redis
restart: unless-stopped
ports:
- "6379:6379"
command: redis-server --requirepass redis_password123
volumes:
- redis_data:/data
networks:
- ai-blog-network
healthcheck:
test: ["CMD", "redis-cli", "-a", "redis_password123", "ping"]
interval: 10s
timeout: 5s
retries: 5
backend:
build:
context: ..
dockerfile: docker/Dockerfile.backend
container_name: ai-blog-backend
restart: unless-stopped
ports:
- "3000:3000"
environment:
NODE_ENV: development
PORT: 3000
MONGODB_URI: mongodb://admin:password123@mongodb:27017/ai_blog?authSource=admin
REDIS_URL: redis://:redis_password123@redis:6379
OPENAI_API_KEY: ${OPENAI_API_KEY}
JWT_SECRET: ${JWT_SECRET:-your-secret-key-change-in-production}
CORS_ORIGIN: http://localhost:5173
volumes:
- ../apps/backend:/app/apps/backend
- ../packages/types:/app/packages/types
- /app/node_modules
depends_on:
mongodb:
condition: service_healthy
redis:
condition: service_healthy
networks:
- ai-blog-network
command: pnpm start:dev
frontend:
build:
context: ..
dockerfile: docker/Dockerfile.frontend
container_name: ai-blog-frontend
restart: unless-stopped
ports:
- "5173:5173"
environment:
VITE_API_URL: http://localhost:3000/api
volumes:
- ../apps/frontend:/app/apps/frontend
- ../packages/types:/app/packages/types
- /app/node_modules
depends_on:
- backend
networks:
- ai-blog-network
command: pnpm dev -- --host 0.0.0.0
networks:
ai-blog-network:
driver: bridge
volumes:
mongodb_data:
redis_data:
生产环境
yaml
# docker-compose.prod.yml
version: '3.8'
services:
mongodb:
image: mongo:7
container_name: ai-blog-mongodb
restart: unless-stopped
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
volumes:
- mongodb_data:/data/db
networks:
- ai-blog-network
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
interval: 10s
timeout: 10s
retries: 5
redis:
image: redis:7-alpine
container_name: ai-blog-redis
restart: unless-stopped
ports:
- "6379:6379"
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
networks:
- ai-blog-network
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
backend:
build:
context: ./backend
dockerfile: Dockerfile.prod
container_name: ai-blog-backend
restart: unless-stopped
ports:
- "3000:3000"
environment:
NODE_ENV: production
PORT: 3000
MONGODB_URI: mongodb://admin:${MONGO_PASSWORD}@mongodb:27017/aiblog?authSource=admin
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
JWT_SECRET: ${JWT_SECRET}
JWT_EXPIRES_IN: 7d
LLM_PROVIDER: openai
LLM_MODEL: gpt-4
OPENAI_API_KEY: ${OPENAI_API_KEY}
RATE_LIMIT_WINDOW_MS: 900000
RATE_LIMIT_MAX_REQUESTS: 100
depends_on:
mongodb:
condition: service_healthy
redis:
condition: service_healthy
networks:
- ai-blog-network
volumes:
- backend_logs:/app/logs
- backend_uploads:/app/uploads
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.prod
container_name: ai-blog-frontend
restart: unless-stopped
ports:
- "80:80"
depends_on:
- backend
networks:
- ai-blog-network
volumes:
mongodb_data:
redis_data:
backend_logs:
backend_uploads:
networks:
ai-blog-network:
driver: bridge
生产环境 .env 文件:
bash
# .env.production
MONGO_PASSWORD=strong-password-here
REDIS_PASSWORD=another-strong-password
JWT_SECRET=ultra-secure-random-string-at-least-32-chars
OPENAI_API_KEY=sk-your-openai-key
启动生产环境:
bash
docker-compose -f docker-compose.prod.yml --env-file .env.production up -d
3.7 Docker 网络与存储管理
网络模式对比
Overlay Network(Swarm)
Node 1
Node 2
Container 4
Container 5
Host Network
Container 3
HostOS
Bridge Network(默认)
Container 1
Bridge
Container 2
Internet
| 网络模式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
bridge |
单机多容器通信 | 隔离性好,易于管理 | 需要端口映射 |
host |
高性能需求 | 无 NAT 开销 | 端口冲突风险 |
overlay |
Docker Swarm 集群 | 跨主机通信 | 配置复杂 |
数据卷管理
bash
# 查看所有数据卷
docker volume ls
# 查看数据卷详情
docker volume inspect mongodb_data
# 备份数据卷
docker run --rm \
-v mongodb_data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/mongodb-backup.tar.gz /data
# 恢复数据卷
docker run --rm \
-v mongodb_data:/data \
-v $(pwd):/backup \
alpine tar xzf /backup/mongodb-backup.tar.gz -C /
# 清理未使用的数据卷
docker volume prune
4. CI/CD 流水线配置
4.1 GitHub Actions 完整配置
yaml
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop, 'feature/*', 'fix/*' ]
pull_request:
branches: [ main ]
env:
NODE_VERSION: '22'
PNPM_VERSION: '10.10.0'
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
test:
name: Test Suite
runs-on: ubuntu-latest
services:
mongodb:
image: mongo:7
ports:
- 27017:27017
options: >-
--health-cmd "mongosh --eval 'db.runCommand(\"ping\")'"
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: '10.10.0'
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile || pnpm install
- name: Build shared types
run: pnpm --filter @ai-blog/types build
- name: Run backend linting
run: pnpm --filter @ai-blog/backend lint || echo "Backend lint completed with warnings"
- name: Run frontend linting
run: pnpm --filter @ai-blog/frontend lint || echo "Frontend lint completed with warnings"
- name: Check backend TypeScript
run: pnpm --filter @ai-blog/backend exec tsc --noEmit || echo "Backend type check completed with warnings"
- name: Check frontend TypeScript
run: pnpm --filter @ai-blog/frontend exec tsc --noEmit || echo "Frontend type check completed with warnings"
- name: Run backend tests
run: pnpm --filter @ai-blog/backend test
env:
MONGODB_URI: mongodb://localhost:27017/test
REDIS_URL: redis://localhost:6379
JWT_SECRET: test-secret-for-ci
OPENAI_API_KEY: test-key-placeholder
- name: Run frontend tests
run: pnpm --filter @ai-blog/frontend test || echo "No frontend tests configured"
continue-on-error: true
- name: Upload coverage reports
uses: codecov/codecov-action@v4
if: always() && hashFiles('./apps/backend/coverage') != ''
continue-on-error: true
with:
directory: ./apps/backend/coverage
flags: backend
name: backend-coverage
token: ${{ secrets.CODECOV_TOKEN }}
build-and-deploy:
name: Build and Deploy
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: '10.10.0'
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile || pnpm install
# Frontend: Deploy to Vercel
- name: Check Vercel secrets
id: vercel-check
run: |
if [ -z "${{ secrets.VERCEL_TOKEN }}" ]; then
echo "has_vercel=false" >> $GITHUB_OUTPUT
echo "::warning::VERCEL_TOKEN secret is not configured. Skipping Vercel deployment."
else
echo "has_vercel=true" >> $GITHUB_OUTPUT
fi
- name: Install Vercel CLI
if: steps.vercel-check.outputs.has_vercel == 'true'
run: pnpm add -g vercel@latest
- name: Remove problematic env vars from Vercel
if: steps.vercel-check.outputs.has_vercel == 'true'
run: |
echo "Removing VITE_API_URL from Vercel project config to avoid secret reference error"
vercel env rm VITE_API_URL production --token="${{ secrets.VERCEL_TOKEN }}" --yes || echo "Variable not found or already removed"
vercel env rm VITE_API_URL preview --token="${{ secrets.VERCEL_TOKEN }}" --yes || echo "Variable not found or already removed"
vercel env rm VITE_API_URL development --token="${{ secrets.VERCEL_TOKEN }}" --yes || echo "Variable not found or already removed"
working-directory: ./apps/frontend
continue-on-error: true
- name: Pull Vercel Environment Information
if: steps.vercel-check.outputs.has_vercel == 'true'
run: vercel pull --yes --environment=production --token="${{ secrets.VERCEL_TOKEN }}"
working-directory: ./apps/frontend
env:
VITE_API_URL: ${{ secrets.VITE_API_URL || 'https://your-api-domain.com/api' }}
- name: Build Frontend
if: steps.vercel-check.outputs.has_vercel == 'true'
run: vercel build --prod --token="${{ secrets.VERCEL_TOKEN }}"
working-directory: ./apps/frontend
env:
VITE_API_URL: ${{ secrets.VITE_API_URL || 'https://your-api-domain.com/api' }}
- name: Deploy Frontend to Vercel
if: steps.vercel-check.outputs.has_vercel == 'true'
run: vercel deploy --prebuilt --prod --token="${{ secrets.VERCEL_TOKEN }}"
working-directory: ./apps/frontend
env:
VITE_API_URL: ${{ secrets.VITE_API_URL || 'https://your-api-domain.com/api' }}
# Backend: Build and push Docker image
- name: Check Docker secrets
id: docker-check
run: |
if [ -z "${{ secrets.DOCKER_USERNAME }}" ] || [ -z "${{ secrets.DOCKER_PASSWORD }}" ]; then
echo "has_docker=false" >> $GITHUB_OUTPUT
echo "::warning::DOCKER_USERNAME or DOCKER_PASSWORD secret is not configured. Skipping Docker deployment."
else
echo "has_docker=true" >> $GITHUB_OUTPUT
fi
- name: Set up Docker Buildx
if: steps.docker-check.outputs.has_docker == 'true'
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
if: steps.docker-check.outputs.has_docker == 'true'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata for Docker
if: steps.docker-check.outputs.has_docker == 'true'
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKER_USERNAME }}/ai-blog-backend
tags: |
type=sha,prefix=
type=ref,event=branch
latest
- name: Build and push backend
if: steps.docker-check.outputs.has_docker == 'true'
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/Dockerfile.backend
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-backend:
needs: build-and-deploy
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy Backend
run: |
echo "✅ Backend Docker image deployed"
echo "✅ Frontend deployed to Vercel"
echo "🚀 Full deployment complete!"
4.2 CI/CD 流程图
PR 到 main
Push 到 main
是
否
开发者推送代码
触发条件?
仅运行测试
完整流水线
代码检查
类型检查
单元测试
覆盖率报告
全部通过?
允许合并
阻止合并
测试套件
构建前端
部署到 Vercel
构建 Docker 镜像
推送到 Docker Hub
部署后端
发送通知
部署完成
4.3 配置项详细解释
触发器配置
yaml
on:
push:
branches: [ main, develop, 'feature/*', 'fix/*' ]
pull_request:
branches: [ main ]
说明:
push:推送到指定分支时触发pull_request:创建/更新 PR 时触发- 使用通配符
'feature/*'匹配所有功能分支
环境变量
yaml
env:
NODE_VERSION: '22'
PNPM_VERSION: '10.10.0'
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
配置位置: GitHub → Settings → Secrets and variables → Actions
必需的秘密变量:
| 变量名 | 用途 | 获取方式 |
|---|---|---|
VERCEL_TOKEN |
Vercel API Token | Vercel Dashboard → Account → Tokens |
VERCEL_ORG_ID |
Vercel 组织 ID | Vercel Dashboard → Settings → General |
VERCEL_PROJECT_ID |
Vercel 项目 ID | Vercel Dashboard → Project → Settings |
DOCKER_USERNAME |
Docker Hub 用户名 | Docker Hub 账号 |
DOCKER_PASSWORD |
Docker Hub 密码 | Docker Hub → Account Settings → Security |
TURBO_TOKEN |
Turborepo Remote Cache | Turborepo Dashboard |
TURBO_TEAM |
Turborepo Team ID | Turborepo Dashboard |
测试服务配置
yaml
services:
mongodb:
image: mongo:7
ports:
- 27017:27017
options: >-
--health-cmd "mongosh --eval 'db.runCommand(\"ping\")'"
--health-interval 10s
--health-timeout 5s
--health-retries 5
说明:
- GitHub Actions 自动启动这些服务容器
health-cmd:健康检查命令,确保服务就绪- 测试完成后自动销毁,无需手动清理
依赖缓存优化
yaml
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
优势:
- ⚡ 缓存 pnpm 全局存储目录
- ⚡ 基于
pnpm-lock.yaml哈希作为缓存键 - ⚡ 依赖未变化时直接使用缓存,节省 2-3 分钟
条件部署
yaml
- name: Check Vercel secrets
id: vercel-check
run: |
if [ -z "${{ secrets.VERCEL_TOKEN }}" ]; then
echo "has_vercel=false" >> $GITHUB_OUTPUT
echo "::warning::VERCEL_TOKEN secret is not configured. Skipping Vercel deployment."
else
echo "has_vercel=true" >> $GITHUB_OUTPUT
fi
- name: Deploy Frontend to Vercel
if: steps.vercel-check.outputs.has_vercel == 'true'
run: vercel deploy --prebuilt --prod --token="${{ secrets.VERCEL_TOKEN }}"
好处:
- ✅ secrets 未配置时不会失败,而是跳过部署
- ✅ 适合开源项目,贡献者无需配置所有 secrets
- ✅ 使用
::warning::输出警告信息
Docker 构建优化
yaml
- name: Build and push backend
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/Dockerfile.backend
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
优化点:
cache-from: type=gha:使用 GitHub Actions Cache 作为构建缓存cache-to: type=gha,mode=max:保存所有层的缓存- 后续构建速度提升 50-70%
4.4 部署策略
蓝绿部署(Blue-Green Deployment)
新部署环境
当前生产环境
切换流量
Blue: v1.0.0
Green: v1.1.0
Load Balancer
用户
实现步骤:
- 部署新版本到 Green 环境
- 运行健康检查和集成测试
- 切换负载均衡器指向 Green
- 观察监控指标
- 如有问题,快速切回 Blue
滚动更新(Rolling Update)
yaml
# Kubernetes 示例
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
特点:
- ✅ 零停机时间
- ✅ 逐步替换旧版本
- ❌ 存在版本共存期,需保证向后兼容
5. 常见问题与解决方案
5.1 Git 分支管理问题
Q1: 如何撤销已推送的提交?
bash
# 方法一:创建新的反向提交(推荐)
git revert <commit-hash>
git push origin main
# 方法二:强制回退(危险,仅限个人分支)
git reset --hard HEAD~1
git push --force-with-lease
⚠️ 警告: 永远不要在共享分支(main/develop)上使用 git push --force
Q2: 如何 cherry-pick 特定提交?
bash
# 从 feature 分支挑选某个提交到 main
git checkout main
git cherry-pick <commit-hash>
git push origin main
应用场景: hotfix 已合并到 develop,但需要紧急应用到生产环境
Q3: 如何处理大型二进制文件?
bash
# 使用 Git LFS(Large File Storage)
git lfs install
git lfs track "*.psd"
git add .gitattributes
git add file.psd
git commit -m "Add large file with LFS"
5.2 Vercel 部署问题
Q1: 构建失败 - "Module not found"
原因: 依赖未正确安装或 monorepo 链接问题
解决方案:
json
{
"installCommand": "pnpm install",
"buildCommand": "pnpm --filter @ai-blog/types build && pnpm --filter @ai-blog/frontend build"
}
Q2: 环境变量未生效
检查清单:
- 确认变量名以
VITE_开头(Vite 项目) - 在 Vercel Dashboard 中检查是否配置了正确的环境(Production/Preview)
- 重新部署使变量生效
- 检查代码中是否正确引用:
import.meta.env.VITE_API_URL
Q3: API 请求 404
原因: Vercel 函数超时或后端服务不可达
解决方案:
json
{
"rewrites": [
{
"source": "/api/(.*)",
"destination": "https://your-backend-domain.com/api/$1"
}
]
}
确保后端 URL 是公网可访问的地址,不是 localhost
5.3 Docker 部署问题
Q1: 容器启动后立即退出
bash
# 查看日志
docker logs ai-blog-backend
# 常见原因:
# 1. 环境变量缺失
# 2. 数据库连接失败
# 3. 端口被占用
# 4. 应用程序崩溃
调试步骤:
bash
# 交互式启动容器
docker run -it --entrypoint sh ai-blog-backend:latest
# 在容器内手动启动应用
node dist/main.js
Q2: MongoDB 连接超时
检查清单:
- 确认 MongoDB 容器已启动:
docker ps | grep mongodb - 检查网络连接:
docker network inspect ai-blog-network - 验证连接字符串中的主机名(应使用容器名
mongodb,不是localhost) - 检查防火墙规则
正确连接字符串:
bash
# Docker 内部网络
MONGODB_URI=mongodb://admin:password@mongodb:27017/ai_blog?authSource=admin
# 本地开发
MONGODB_URI=mongodb://admin:password@localhost:27017/ai_blog?authSource=admin
Q3: Docker 镜像体积过大
优化前: 1.2GB
优化后: 350MB
优化技巧:
dockerfile
# 1. 使用 Alpine 基础镜像
FROM node:22-alpine
# 2. 多阶段构建
FROM node:22-alpine AS builder
# ... 构建步骤 ...
FROM node:22-alpine
COPY --from=builder /app/dist ./dist
# 3. 合并 RUN 指令
RUN apt-get update && \
apt-get install -y --no-install-recommends package && \
rm -rf /var/lib/apt/lists/*
# 4. 使用 .dockerignore
echo "node_modules\n.git\ndist" > .dockerignore
# 5. 清理缓存
RUN npm ci --only=production && npm cache clean --force
5.4 CI/CD 流水线问题
Q1: 构建时间过长
优化策略:
yaml
# 1. 启用依赖缓存
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
# 2. 并行执行独立任务
jobs:
lint:
runs-on: ubuntu-latest
steps: [...]
test:
runs-on: ubuntu-latest
steps: [...]
build:
needs: [lint, test] # 并行完成后才构建
runs-on: ubuntu-latest
steps: [...]
# 3. 使用 TurboRepo Remote Cache
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
效果对比:
- 优化前:15 分钟
- 优化后:5 分钟(缓存命中时 2 分钟)
Q2: 部署失败 - 权限不足
错误信息:
Error: The caller does not have permission
解决方案:
- 检查 GitHub Secrets 是否正确配置
- 验证 Vercel Token 是否有部署权限
- 确认 Docker Hub 账号密码正确
- 检查 Token 是否过期
重新生成 Vercel Token:
Vercel Dashboard → Account → Tokens → Create Token
Scope: Read & Write
Q3: 测试通过但部署后出错
原因: 测试环境与生产环境差异
解决方案:
yaml
# 1. 在 CI 中使用与生产相同的环境变量
- name: Run integration tests
env:
NODE_ENV: production
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
# 2. 添加健康检查
- name: Health check after deployment
run: |
sleep 10 # 等待服务启动
curl -f https://your-domain.com/health || exit 1
# 3. 使用 E2E 测试
- name: Run E2E tests
run: pnpm test:e2e
5.5 性能优化建议
前端优化
nginx
# Nginx 配置优化
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
Vite 构建优化:
typescript
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
antd: ['antd'],
},
},
},
},
});
后端优化
typescript
// Nest.js 启用压缩
import compression from 'compression';
app.use(compression());
// 启用缓存
import { CacheInterceptor, CacheModule } from '@nestjs/cache-manager';
@Module({
imports: [
CacheModule.register({
ttl: 300, // 5 minutes
max: 100, // maximum number of items in cache
}),
],
})
export class AppModule {}
数据库优化
javascript
// MongoDB 索引优化
db.articles.createIndex({ slug: 1 }, { unique: true });
db.articles.createIndex({ createdAt: -1 });
db.articles.createIndex({ author: 1, createdAt: -1 });
// Redis 缓存策略
const cacheKey = `article:${slug}`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const article = await Article.findOne({ slug });
await redis.setex(cacheKey, 300, JSON.stringify(article)); // 5 min TTL
return article;
6. 总结与最佳实践
6.1 核心原则
开发效率
高质量交付
代码质量
系统稳定性
自动化流程
快速反馈
Code Review
自动化测试
静态分析
健康检查
监控告警
快速回滚
6.2 检查清单
分支管理
- 遵循分支命名规范
- 功能分支从 main/develop 创建
- 提交信息清晰描述变更
- PR 包含完整的测试和文档
- Code Review 至少 1 人批准
- 合并后删除远程分支
Vercel 部署
- vercel.json 配置正确
- SPA 路由回退规则已添加
- 环境变量已配置(区分环境)
- CORS 后端已正确配置
- 自定义域名 DNS 已设置
- 构建缓存已启用
Docker 部署
- 使用多阶段构建减小镜像体积
- 敏感信息通过环境变量传递
- 数据卷已持久化
- 健康检查已配置
- 日志已收集和管理
- 资源限制已设置(CPU/内存)
CI/CD
- 测试覆盖率 > 80%
- 依赖缓存已启用
- Secrets 已正确配置
- 部署失败有告警通知
- 支持快速回滚
- 构建时间 < 10 分钟
6.3 推荐工具链
| 类别 | 工具 | 用途 |
|---|---|---|
| 版本控制 | Git + GitHub | 代码管理 |
| 包管理 | pnpm | 快速、节省磁盘空间 |
| Monorepo | Turborepo | 智能缓存、并行构建 |
| CI/CD | GitHub Actions | 自动化流水线 |
| 前端部署 | Vercel | 零配置、全球 CDN |
| 容器化 | Docker | 环境一致性 |
| 编排 | Docker Compose | 多容器管理 |
| 监控 | Sentry + Prometheus | 错误追踪、性能监控 |
| 日志 | ELK Stack | 日志聚合分析 |
6.4 学习资源
- Git: Pro Git Book
- GitHub Actions: Official Docs
- Vercel: Vercel Docs
- Docker: Docker Best Practices
- Nest.js: Nest.js Documentation
结语
本文基于真实的 Monorepo 全栈项目,系统地介绍了:
- Git 分支管理:从理论到实践,包括多种策略对比和应急处理流程
- Vercel 部署:SPA 路由、CORS 配置、环境变量管理等细节
- Docker 容器化:MongoDB、Redis、前后端服务的完整配置
- CI/CD 流水线:GitHub Actions 的详细解析和优化技巧
希望这份指南能帮助你构建高效、可靠的现代化开发工作流。记住,工具是为目标服务的,选择最适合你团队的方案,持续迭代优化,才能最大化开发效率。
📝 关于作者
本文基于 AI Blog Platform 项目实战经验总结而成,该项目是一个基于 Nest.js + React + LangChain 的全栈 AI 内容创作平台,采用 Turborepo Monorepo 架构,支持智能写作、RAG 知识库、可视化工作流编排等功能。
🔗 相关链接
- 项目源码:GitHub Repository
- 在线演示:Live Demo
- 技术博客:Blog
License: MIT © 2024