部署相关
掌握软件部署中常见的基础概念、部署方案与应用场景。
对象存储(阿里云 OSS/腾讯云 COS/亚马逊 AWS S3)
对象存储是一种云存储服务,专门用于存储非结构化数据,比如:文件、图片、视频等等。
- 高扩展:只要你愿意给云厂商付费,存储容量无限
- 高可用:数据会自动做冗余备份
- 按需付费:不需要自己买服务器、磁盘,按存储容量、请求次数、流出流量等计费,无固定服务器成本
- 唯一 URL:每个文件都有唯一的 URL,支持通过 HTTP/HTTPS 直接访问
CDN
CDN 是一组分布在全球各地的服务器,用于加速静态资源的访问。
工作原理:
- 用户请求一个文件,如:https://cdn.example.com/logo.png
- CDN 会根据用户的地理位置,将请求转发到离用户最近的服务器上
- 如果该服务器有缓存,直接返回
- 如果没有缓存,CDN 会存源站拉取后再返回,并缓存
源站:可以是在云厂商的 CDN 服务中配置我们自己的云服务器,也可以结合对象存储服务,从对象存储中拉取。
注意:CDN 本身不存储原始文件,它只是源站的 “缓存代理”。
优点:
- 速度快:用户会从最近的节点获取资源
- 减轻源站压力:大部分静态资源的请求都可以交给 CDN 节点处理,静态资源(图片、JS、CSS)不再占用源站带宽和算力,让源站专注处理动态业务
- 高可用:CDN 会在节点故障时自动切换其他节点
- 降低成本:通过 CDN 的缓存和流量调度,减少源站的带宽费用,尤其在全球访问场景下更明显
- 安全防护:CDN 可以提供 DDoS 防护、WAF 等安全能力,保护源站安全
WAF(Web Application Firewall):Web 应用防火墙,用于防护 Web 应用的安全,比如防止 SQL 注入、XSS 攻击等。
Nginx 配置对象存储 + CDN(前后端分离架构)
# 前端项目构建后,将静态资源上传到对象存储
# 配置 CDN,将对象存储作为源站
# 配置 Nginx,将静态资源请求转发到 CDN
# 动态请求(API)转发到后端服务器
server {
listen 80;
server_name bush.1px.club;
# 静态资源请求转发到 CDN
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
# 重定向到 CDN ,让浏览器以后直接访问 CDN 地址,减少 Nginx 的转发压力
return 301 https://cdn.1px.club$request_uri;
}
# API 请求转发到后端
location /api/ {
proxy_pass http://jungle_app:3000;
}
# 其他请求返回主页面(SPA)
location / {
# 返回 index.html,让前端路由处理
proxy_pass https://cdn.1px.club/index.html;
}
}CI/CD (持续集成/持续部署)
CI(Continuous Integration):在频繁提交代码时,每次提交都自动运行测试、构建等操作。
CD(Continuous Deployment):在 CI 成功后,自动部署到生产环境。
CI/CD 是软件开发的一套流程和理念,实现这套流程的工具有很多,比如:Jenkins/GitLab CI/GitHub Actions/Travis CI/AWS CodePipeline 等。
云端执行 CI/CD
以 Github Actions 为例:
- 当代码推送到远程仓库时,Github Actions 到调度器 Orchestrator 会检测事件(push),检查是否存在匹配事件的 Workflow ,把 Workflow 加入到代执行队列中
- Workflow 执行,调度器根据 Workflow 中的 runs-on 配置,分配合适的执行器(Runner)来执行任务
我们的任务都会在 Github Actions 提供的 Runner 中运行。
本地执行 CI/CD
前面我介绍了基于云端 Runner 的 CI/CD 流程,其实我们在本地也可以做 CI/CD ,我们可以借助 Git Hooks 来实现。
这种方案的优势在于个人开发零成本实现 CI/CD:无需依赖云服务或额外服务器,利用本地环境即可实现自动化。
当然,在团队协作场景下,这种方案在可靠性、安全性和协作效率上也存在着明显的短板。
因此,本地 CI/CD 更适合作为个人开发者的轻量工具,而团队协作则应优先选择云端 CI/CD 方案,以保障流程的可靠性与安全性。
CI/CD 部署实践:blog
正常来说,blog 这种类型的项目,是用 VitePress 生成的纯静态站点,不需要和后端进行 API 交互,这种情况下连 Nginx 都可以不需要了,把 index.html 和 JS/CSS/PNG 等静态资源全部都放到对象存储中,直接把域名解析到 CDN 即可。
限制条件
然鹅我不希望产生对象存储/CDN 费用,所以下面还是把 blog 部署到我的云服务器中。
在下面的部署实践开始之前,你需要先了解一下我这边的限制条件:
- 我不希望产生对象存储/CDN 费用
- 我的云服务器资源非常非常非常有限,OOM 风险很高
- Github 私有仓库存在 Runner 资源限制(免费额度、免费存储限制),可以使用 self-hosted runner ,但依然需要考虑第 2 点
- Github 国内网络限制,大文件上传托管不稳定
方案选择:本地 CI/CD
基于以上限制,我选择了本地 CI/CD 方案:
- CI(持续集成):在 commit 阶段执行,确保代码质量
- CD(持续部署):在 push 阶段执行,确保代码部署
工作流程
完整流程图
开发阶段
↓
git add .
↓
git commit -m "feat: xxx"
↓
┌─────────────────────────────────────────┐
│ CI (持续集成) - commit 阶段 │
├─────────────────────────────────────────┤
│ pre-commit: │
│ lint-staged (暂存文件检查) │
│ ESLint (代码检查) │
│ Prettier (代码格式化) │
│ │
│ commit-msg: │
│ Commitlint (提交信息规范检查) │
└─────────────────────────────────────────┘
↓
提交成功
↓
git push
↓
┌─────────────────────────────────────────┐
│ CD (持续部署) - push 阶段 │
├─────────────────────────────────────────┤
│ pre-push: │
│ 1. 询问是否部署 │
│ 2. SSH 连通性检测(快速失败) │
│ 3. Docker 容器内构建静态产物 │
│ 4. Rsync 同步到服务器版本目录 │
│ 5. 切换软链接(current → 新版本) │
│ 6. 重启容器(docker-compose up) │
│ 7. HTTP 健康验证(curl 全链路探测) │
└─────────────────────────────────────────┘
↓
部署成功 + push 成功相关命令
# 开发
pnpm run c:up # 启动本地开发环境(Docker)
pnpm run c:down # 停止本地开发环境
# 部署
pnpm run deploy:prod # 手动执行完整部署流程(日常会通过 CI/CD 执行)
pnpm run rollback # 交互式回滚到历史版本敏感文件管理
项目中有两类敏感文件,均不提交到 Git,在新机器上需要手动创建:
| 文件 | 说明 | 参考模板 |
|---|---|---|
.env.deploy | 服务器 IP、SSH 端口、路径等部署配置 | .env.deploy.example |
nginx.htpasswd | 简历页面 HTTP Basic Auth 密码哈希 | nginx.htpasswd.example |
nginx.htpasswd虽然存储的是哈希值(非明文),但泄露后仍可被离线暴力破解,因此同样不进入版本控制。生成 htpasswd 文件:
htpasswd -c nginx.htpasswd <用户名>
回滚机制
部署时不直接覆盖静态文件,而是采用多版本 + 软链接方案:
$PROJECT_PATH/
├── releases/
│ ├── 20260220_143000/ ← 历史版本(自动保留最近 3 个)
│ └── 20260220_183700/ ← 本次部署版本
├── current -> releases/20260220_183700 (软链接,Nginx 挂载此目录)
├── Dockerfile
├── docker-compose.yml
└── nginx.conf工作流程:
- 每次
pnpm run deploy:prod在releases/下创建带时间戳的新目录 - rsync 将构建产物上传到该目录
ln -sfn原子切换current软链接指向新目录(秒级生效,无中断)- 自动清理最旧的版本,始终保留最近 3 个
回滚操作:
pnpm run rollback
# 输出示例:
# 📋 可用历史版本(从新到旧):
# 1) 20260220_183700 ← 当前版本
# 2) 20260220_143000
# 3) 20260219_210000
# 请输入要回滚到的版本编号(直接回车取消): 2
# 🔄 回滚到版本: 20260220_143000
# ✅ 回滚完成!回滚只是切换软链接 + docker-compose restart,无需重新构建,秒级完成。
CI/CD 部署实践:vine
Vine 是配套 Bush & Jungle 的仓库作业移动端应用,采用与 Blog 相同的本地 CI/CD 方案。
技术栈差异
与 Blog(VitePress 静态站点)不同,Vine 是基于 React + Vite 的 SPA 应用,需要:
- API 反向代理(解决 Cookie 跨域问题)
- 微前端路由支持(Qiankun)
- 更复杂的 Nginx 配置
部署架构
Vine 采用与 Blog 完全一致的部署架构:
$PROJECT_PATH/
├── releases/
│ ├── 20260225_210000/ ← 历史版本
│ ├── 20260226_143000/ ← 历史版本
│ └── 20260226_183700/ ← 当前版本
├── current -> releases/20260226_183700 (软链接,Nginx 挂载此目录)
├── Dockerfile
├── docker-compose.yml
└── nginx.conf工作流程
与 Blog 完全一致:
开发阶段
↓
git commit (CI)
├── lint-staged
├── ESLint
├── Prettier
└── Commitlint
↓
git push (CD)
├── 询问是否部署
├── SSH 连通性检测
├── Docker 容器内构建
├── Rsync 同步到服务器
├── 切换软链接
├── 重启容器
└── HTTP 健康检查
↓
部署成功相关命令
# 开发
pnpm run c:up # 启动本地开发环境(Docker)
pnpm run c:down # 停止本地开发环境
# 部署
pnpm run deploy:prod # 手动执行完整部署流程
pnpm run rollback # 交互式回滚到历史版本核心特性
与 Blog 保持一致:
- ✅ 多版本管理 + 软链接(秒级回滚)
- ✅ 容器内构建(环境一致性)
- ✅ 智能镜像缓存(基于 Dockerfile.dev 的 MD5 检测)
- ✅ 完整健康检查(SSH 预检 + HTTP 全链路验证)
- ✅ 敏感信息管理(.env.deploy)
- ✅ 自动清理机制(保留最近 3 个版本)
- ✅ 无需额外云端成本
Nginx 配置差异
Vine 的 Nginx 配置相比 Blog 更复杂,需要处理:
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
# API 反向代理 - 实现同域请求,解决 Cookie 跨域问题
location /api/ {
resolver 127.0.0.11 valid=30s;
set $upstream_jungle http://jungle_app:3000;
rewrite ^/api/(.*)$ /$1 break;
proxy_pass $upstream_jungle;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 主应用路由(SPA)
# 注意:/product/* 业务路由也由主应用处理,子应用资源由 Traefik 路由到 vine-product 容器
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 启用 gzip 压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}重构历程
Vine 项目最初采用的是混合方案(本地构建 + GitHub Actions 部署),存在以下问题:
- ❌ Git 仓库污染:dist.tar.gz 提交到版本控制,每次部署产生一个 commit
- ❌ 无版本管理:直接覆盖部署,无法回滚
- ❌ 缺少健康检查:部署后未验证服务可用性
- ❌ 无镜像缓存:每次可能重建镜像,浪费时间
- ⚠️ 依赖 GitHub Actions:消耗免费额度,受网络限制
重构后采用与 Blog 一致的本地 CI/CD 方案,解决了所有问题。
敏感文件管理
与 Blog 相同,Vine 也使用 .env.deploy 管理部署配置:
| 文件 | 说明 | 参考模板 |
|---|---|---|
.env.deploy | 服务器 IP、SSH 端口、路径等部署配置 | .env.deploy.example |
配置示例:
SERVER_IP=your_server_ip
SSH_PORT=2333
SERVER_USER=tangzhenming
PROJECT_PATH=/home/tangzhenming/project/vine部署前准备
首次部署需要:
- 创建
.env.deploy配置文件 - 配置 SSH 密钥(服务器公钥已加入
~/.ssh/known_hosts) - 清理 Git 历史中的 dist.tar.gz(如果之前提交过)
清理 Git 历史:
# 执行清理脚本
./scripts/cleanup-git-history.sh
# 强制推送到远程
git push origin --force --all最佳实践总结
通过 Blog 和 Vine 两个项目的实践,总结出以下最佳实践:
- 统一部署方案:多个项目采用相同的部署架构,降低维护成本
- 多版本管理:始终保留历史版本,支持快速回滚
- 容器内构建:保证构建环境一致性,避免"在我机器上能跑"
- 智能缓存:基于文件 hash 检测变化,避免不必要的重建
- 完整验证:部署前检测连通性,部署后验证服务可用性
- 敏感信息隔离:使用 .env 文件管理配置,不提交到 Git
- 自动化清理:定期清理旧版本,避免磁盘空间浪费
适用场景
本地 CI/CD 方案适合:
- ✅ 个人项目或小团队
- ✅ 资源受限场景(服务器配置低)
- ✅ 希望零云端成本
- ✅ 网络环境受限(GitHub Actions 不稳定)
不适合:
- ❌ 大型团队协作(缺少可观测性)
- ❌ 需要严格的权限控制
- ❌ 需要详细的部署审计日志
CI/CD 部署实践:vine-product
Vine Product 是 Vine 主应用的微前端子应用(Vue 3),同样采用本地 CI/CD 方案。
与 Vine 主应用的差异
| 维度 | Vine 主应用 | Vine Product 子应用 |
|---|---|---|
| 框架 | React + Vite | Vue 3 + Vite |
| 角色 | Qiankun 主应用(基座) | Qiankun 子应用 |
| 部署方式 | 独立容器 | 独立容器 |
| 路由 | / | /product/ |
| CI 流程 | Git Hooks(pre-commit、pre-push) | Git Hooks(pre-commit、pre-push) |
| 部署触发 | push 时自动询问 | push 时自动询问 |
| 代码检查 | ESLint + Prettier | ESLint + Prettier |
部署架构
vine.1px.club/
├── / ← Vine 主应用(React)
│ └── 容器:vine_app
├── /product/ ← Vine Product 子应用(Vue)
│ └── 容器:vine_product_app
└── /api/ ← API 反向代理到 Jungle通过 Traefik 网关统一路由:
# Traefik 路由优先级
vine-product: priority=100 # 子应用优先
vine: priority=1 # 主应用默认微前端部署要点
独立部署,统一加载
- 主应用和子应用各自独立容器
- 各自独立的版本管理和回滚
- 子应用通过 qiankun 动态加载,不支持独立访问
Memory History 路由
- 子应用使用 Memory History(路由状态由主应用管理)
- 不支持直接访问子应用 URL(如
/product/finish) - 必须通过主应用访问(主应用内部加载子应用)
同域部署
- 通过 Traefik 统一域名
- 无跨域问题,Cookie 可正常传递
- 子应用入口:
/product/(仅供 qiankun 加载)
健康检查策略
- 容器状态:验证 Docker 容器运行正常
- 入口可达:验证 qiankun 能获取入口 HTML
- 静态资源:验证 assets 目录存在
- 手动验证:部署后通过主应用验证子应用能否正常加载
部署命令
# Vine 主应用
cd vine
git add .
git commit -m "feat: xxx" # 触发 CI
git push # 触发 CD(询问是否部署)
pnpm run deploy:prod # 手动部署
pnpm run rollback # 回滚
# Vine Product 子应用
cd vine-product
git add .
git commit -m "feat: xxx" # 触发 CI
git push # 触发 CD(询问是否部署)
pnpm run deploy:prod # 手动部署
pnpm run rollback # 回滚完整的 CI/CD 流程
主应用和子应用都采用相同的流程:
开发阶段
↓
git commit (CI)
├── lint-staged(暂存文件检查)
├── ESLint(代码检查)
├── Prettier(代码格式化)
└── Commitlint(提交信息规范)
↓
git push (CD)
├── 询问是否部署(仅 main 分支)
├── SSH 连通性检测
├── Docker 容器内构建
├── Rsync 同步到服务器
├── 切换软链接
├── 重启容器
└── HTTP 健康检查
↓
部署成功CI/CD 部署实践:bush
Bush 是仓库作业管理系统的前端应用(Ant Design Pro + UmiJS),采用与 Blog 相同的本地 CI/CD 方案。
技术栈差异
与 Blog(VitePress 静态站点)不同,Bush 是基于 React + UmiJS 的 SPA 应用,需要:
- API 反向代理(解决 Cookie 跨域问题)
- 用户上传文件的静态资源服务
- 更复杂的 Nginx 配置
部署架构
Bush 采用与 Blog 完全一致的部署架构:
$PROJECT_PATH/
├── releases/
│ ├── 20260225_210000/ ← 历史版本
│ ├── 20260226_143000/ ← 历史版本
│ └── 20260226_183700/ ← 当前版本
├── current -> releases/20260226_183700 (软链接,Nginx 挂载此目录)
├── Dockerfile
├── docker-compose.yml
└── nginx.conf工作流程
与 Blog 完全一致:
开发阶段
↓
git commit (CI)
├── lint-staged
├── ESLint
├── Prettier
└── Commitlint
↓
git push (CD)
├── 询问是否部署
├── SSH 连通性检测
├── Docker 容器内构建
├── Rsync 同步到服务器
├── 切换软链接
├── 重启容器
└── HTTP 健康检查
↓
部署成功相关命令
# 开发
pnpm run c:up # 启动本地开发环境(Docker)
pnpm run c:down # 停止本地开发环境
# 部署
pnpm run deploy:prod # 手动执行完整部署流程
pnpm run rollback # 交互式回滚到历史版本核心特性
与 Blog 保持一致:
- ✅ 多版本管理 + 软链接(秒级回滚)
- ✅ 容器内构建(环境一致性)
- ✅ 智能镜像缓存(基于 Dockerfile.dev 的 MD5 检测)
- ✅ 精细化容器管理(配置变化才重建,否则只重启)
- ✅ 完整健康检查(SSH 预检 + HTTP 全链路验证)
- ✅ 敏感信息管理(.env.deploy)
- ✅ 自动清理机制(保留最近 3 个版本)
- ✅ 无需额外云端成本
Nginx 配置差异
Bush 的 Nginx 配置相比 Blog 更复杂,需要处理:
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
# API 反向代理 - 实现同域请求,解决 Cookie 跨域问题
location /api/ {
resolver 127.0.0.11 valid=30s;
set $upstream_jungle http://jungle_app:3000;
rewrite ^/api/(.*)$ /$1 break;
proxy_pass $upstream_jungle;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 用户文件上传 - 处理用户上传的静态文件
location ^~ /static/ {
alias /app/shared_uploads/;
expires 1y;
add_header Cache-Control "public, max-age=31536000";
}
# SPA 路由
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
if ($request_uri ~* "^/static/") {
break;
}
expires 1y;
add_header Cache-Control "public, max-age=31536000";
}
}CI/CD 部署实践:earth
Earth 是个人主页应用(Next.js SSR),同样采用本地 CI/CD 方案,但与其他静态站点项目有显著差异。
技术栈差异
| 维度 | Blog/Vine/Bush(静态/SPA) | Earth(SSR) |
|---|---|---|
| 框架 | VitePress/React/UmiJS | Next.js |
| 渲染方式 | 客户端渲染(CSR) | 服务端渲染(SSR) |
| 服务器 | Nginx(静态文件服务) | Node.js(运行时服务) |
| 端口 | 80 | 3000 |
| 构建产物 | dist/(纯静态文件) | .next/standalone/(含运行时) |
| 启动命令 | nginx | node server.js |
| 部署方式 | 挂载静态文件到 Nginx | 挂载 standalone 到 Node 容器 |
部署架构
Earth 采用与其他项目一致的多版本管理架构:
$PROJECT_PATH/
├── releases/
│ ├── 20260225_210000/ ← 历史版本
│ │ ├── server.js # Next.js 服务器入口
│ │ ├── .next/ # Next.js 运行时
│ │ │ └── static/ # 静态资源
│ │ └── public/ # 公共资源
│ ├── 20260226_143000/ ← 历史版本
│ └── 20260226_183700/ ← 当前版本
├── current -> releases/20260226_183700/ (软链接,Docker 挂载此目录)
├── Dockerfile
├── docker-compose.yml
└── .dockerfile_hashNext.js Standalone 模式
Earth 使用 Next.js 的 standalone 输出模式,生成最小化的运行时:
next.config.ts:
const nextConfig: NextConfig = {
output: 'standalone'
}构建产物结构:
.next/
├── standalone/ # 最小化运行时(包含 server.js)
│ ├── server.js # 服务器入口
│ ├── node_modules/ # 运行时依赖(已优化)
│ └── .next/ # Next.js 运行时
├── static/ # 静态资源(需要单独复制)
└── ...部署要求:
- 复制
standalone/目录的所有内容到服务器 - 复制
.next/static/到服务器的.next/static/ - 复制
public/到服务器的public/ - 运行
node server.js
工作流程
与其他项目保持一致:
开发阶段
↓
git commit (CI)
├── lint-staged
├── ESLint
├── Prettier
└── Commitlint
↓
git push (CD)
├── 询问是否部署
├── SSH 连通性检测
├── Docker 容器内构建(生成 standalone)
├── Rsync 同步到服务器(standalone + static + public)
├── 切换软链接
├── 重启容器
└── HTTP 健康检查(重试次数增加到 15 次)
↓
部署成功相关命令
# 开发
pnpm run c:up # 启动本地开发环境(Docker)
pnpm run c:down # 停止本地开发环境
# 部署
pnpm run deploy:prod # 手动执行完整部署流程
pnpm run rollback # 交互式回滚到历史版本核心特性
与其他项目保持一致:
- ✅ 多版本管理 + 软链接(秒级回滚)
- ✅ 容器内构建(环境一致性)
- ✅ 智能镜像缓存(基于 Dockerfile.dev 的 MD5 检测)
- ✅ 精细化容器管理(配置变化才重建,否则只重启)
- ✅ 完整健康检查(SSH 预检 + HTTP 全链路验证)
- ✅ 敏感信息管理(.env.deploy)
- ✅ 自动清理机制(保留最近 3 个版本)
- ✅ 无需额外云端成本
Dockerfile 差异
其他项目(Nginx):
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Earth(Node.js):
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
EXPOSE 3000
# 构建产物通过 volume 挂载
CMD ["node", "server.js"]关键差异:
- 基础镜像:
nginx:alpine→node:20-alpine - 端口:
80→3000 - 启动命令:
nginx→node server.js - 不复制文件:构建产物通过 volume 挂载
docker-compose.yml 差异
其他项目:
services:
app:
volumes:
- ${RELEASE_DIR:-./current}:/usr/share/nginx/html # Nginx 静态文件目录
labels:
- 'traefik.http.services.app.loadbalancer.server.port=80'Earth:
services:
earth:
volumes:
- ${RELEASE_DIR:-./current}:/app # Node.js 工作目录
labels:
- 'traefik.http.services.earth.loadbalancer.server.port=3000'关键差异:
- 挂载目录:
/usr/share/nginx/html→/app - 服务端口:
80→3000
部署脚本差异
构建产物上传:
其他项目(单个 dist 目录):
rsync -avz --delete dist/ $SERVER:$RELEASE_DIR/Earth(三个目录):
# 上传 standalone
rsync -avz --delete .next/standalone/ $SERVER:$RELEASE_DIR/
# 上传 static
rsync -avz --delete .next/static/ $SERVER:$RELEASE_DIR/.next/static/
# 上传 public
rsync -avz --delete public/ $SERVER:$RELEASE_DIR/public/健康检查差异:
其他项目(重试 10 次):
MAX_RETRY=10Earth(重试 15 次,Next.js 启动较慢):
MAX_RETRY=15 # Next.js 启动较慢,增加重试次数性能优化
构建优化:
- 使用 Docker Volume 缓存
node_modules和pnpm store - 使用
--frozen-lockfile确保依赖版本一致 - 使用
standalone模式减小产物体积
部署优化:
- 使用
rsync --delete增量上传 - 使用软链接实现秒级切换
- 使用 hash 检测避免不必要的镜像重建
运行优化:
- 使用
NODE_ENV=production启用生产模式 - 使用 Traefik 反向代理和 HTTPS
- 使用 Docker 容器隔离和资源限制
常见问题
1. 构建失败:内存不足
原因:Next.js 构建需要较多内存
解决:
- 在本地 Docker 容器内构建(已实现)
- 增加 Docker 容器内存限制
2. 服务启动慢
原因:Next.js 服务器启动需要时间
解决:
- 部署脚本已增加重试次数(15 次)
- 每次重试间隔 3 秒
3. 静态资源 404
原因:.next/static/ 或 public/ 未正确上传
解决:
- 检查部署脚本是否正确上传这两个目录
- 检查服务器上的目录结构
4. 环境变量不生效
原因:Next.js 构建时会将环境变量打包进代码
解决:
- 公开环境变量(
NEXT_PUBLIC_*)在构建时注入 - 私有环境变量在运行时通过 docker-compose.yml 注入
最佳实践总结
通过 Blog、Vine、Bush、Earth 等项目的实践,总结出以下最佳实践:
- 统一部署方案:多个项目采用相同的部署架构,降低维护成本
- 多版本管理:始终保留历史版本,支持快速回滚
- 容器内构建:保证构建环境一致性,避免"在我机器上能跑"
- 智能缓存:基于文件 hash 检测变化,避免不必要的重建
- 精细化管理:配置变化才重建镜像,否则只重启容器
- 完整验证:部署前检测连通性,部署后验证服务可用性
- 敏感信息隔离:使用 .env 文件管理配置,不提交到 Git
- 自动化清理:定期清理旧版本,避免磁盘空间浪费
- 适配不同技术栈:根据项目特点(静态/SPA/SSR)调整部署细节
适用场景
本地 CI/CD 方案适合:
- ✅ 个人项目或小团队
- ✅ 资源受限场景(服务器配置低)
- ✅ 希望零云端成本
- ✅ 网络环境受限(GitHub Actions 不稳定)
- ✅ 多种技术栈混合部署
不适合:
- ❌ 大型团队协作(缺少可观测性)
- ❌ 需要严格的权限控制
- ❌ 需要详细的部署审计日志
CI/CD 部署实践:jungle
Jungle 是仓库作业管理系统的后端应用(NestJS),采用本地 CI/CD 方案,与前端项目保持架构一致。
技术栈特点
Jungle 作为后端应用,与前端项目有显著差异:
| 维度 | 前端项目(Blog/Vine/Bush) | Jungle 后端 |
|---|---|---|
| 框架 | VitePress/React/UmiJS | NestJS |
| 构建产物 | 纯静态文件 | Node.js 运行时 + 依赖 |
| 服务器 | Nginx | Node.js |
| 端口 | 80 | 3000 |
| 启动命令 | nginx | node dist/main |
| 依赖管理 | devDependencies | dependencies |
| 环境变量 | 构建时注入 | 运行时注入 |
| 数据库 | 无 | PostgreSQL + Redis |
| 持久化数据 | 无 | 数据库 Volume + 上传文件 |
| 构建位置 | 本地 Docker 容器 | 本地 Docker 容器 |
部署架构
Jungle 采用与前端项目一致的多版本管理架构:
$PROJECT_PATH/
├── releases/
│ ├── 20260220_143000/ ← 历史版本
│ │ ├── dist/ # NestJS 编译产物
│ │ ├── node_modules/ # 生产依赖
│ │ └── package.json
│ ├── 20260226_143000/ ← 历史版本
│ └── 20260226_183700/ ← 当前版本
├── current -> releases/20260226_183700 (软链接,Docker 挂载此目录)
├── uploads/ # 用户上传文件(持久化,跨版本共享)
├── Dockerfile
├── docker-compose.yml
├── .env.production # 生产环境变量(敏感信息)
└── .dockerfile_hash工作流程
与前端项目保持一致:
开发阶段
↓
git commit (CI)
├── lint-staged
├── ESLint
├── Prettier
└── Commitlint
↓
git push (CD)
├── 询问是否部署
├── SSH 连通性检测
├── Docker 容器内构建
│ ├── pnpm install --frozen-lockfile
│ ├── pnpm run build
│ └── pnpm install --prod(生产依赖)
├── Rsync 同步到服务器
│ ├── dist/
│ ├── node_modules/
│ └── package.json
├── 切换软链接
├── 重启容器
└── HTTP 健康检查
↓
部署成功相关命令
# 开发
pnpm run c:up # 启动本地开发环境(Docker)
pnpm run c:down # 停止本地开发环境
# 部署
pnpm run deploy:prod # 手动执行完整部署流程
pnpm run rollback # 交互式回滚到历史版本核心特性
与前端项目保持一致:
- ✅ 多版本管理 + 软链接(秒级回滚)
- ✅ 容器内构建(环境一致性)
- ✅ 智能镜像缓存(基于 Dockerfile.dev 的 MD5 检测)
- ✅ 精细化容器管理(配置变化才重建,否则只重启)
- ✅ 完整健康检查(SSH 预检 + HTTP 全链路验证)
- ✅ 敏感信息管理(.env.deploy + .env.production)
- ✅ 自动清理机制(保留最近 3 个版本)
- ✅ 无需额外云端成本
后端特有配置
1. 持久化数据管理
用户上传文件需要跨版本共享:
# docker-compose.yml
services:
app:
volumes:
- ${RELEASE_DIR:-./current}:/app # 当前版本代码
- ./uploads:/app/uploads # 上传文件(持久化)2. 数据库连接
通过 Docker 网络连接数据库:
services:
app:
networks:
- jungle_network
- traefik_network
networks:
jungle_network:
external: true # 连接到 PostgreSQL + Redis
traefik_network:
external: true # 连接到 Traefik 网关3. 环境变量管理
生产环境变量通过 .env.production 注入:
# .env.production(不提交到 Git)
NODE_ENV=production
PORT=3000
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_NAME=jungle
DATABASE_USER=jungle
DATABASE_PASSWORD=***
REDIS_HOST=redis
REDIS_PORT=6379
JWT_SECRET=***Dockerfile 差异
前端项目(Nginx):
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Jungle(Node.js):
FROM node:20-alpine
WORKDIR /app
# 安装 pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# 构建产物通过 volume 挂载,不在镜像中复制
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
# 启动应用
CMD ["node", "dist/main"]部署脚本差异
构建流程:
前端项目(单次构建):
docker run --rm \
-v "$(pwd):/app" \
-v project_node_modules:/app/node_modules \
project-dev:latest \
sh -c "pnpm install --frozen-lockfile && pnpm run build"Jungle(两次安装):
# 1. 安装所有依赖 + 构建
docker run --rm \
-v "$(pwd):/app" \
-v jungle_node_modules:/app/node_modules \
jungle-dev:latest \
sh -c "pnpm install --frozen-lockfile && pnpm run build"
# 2. 清理 + 安装生产依赖
docker run --rm \
-v "$(pwd):/app" \
jungle-dev:latest \
sh -c "rm -rf node_modules && pnpm install --prod --frozen-lockfile"上传内容:
前端项目(单个目录):
tar -czf /tmp/project_dist.tar.gz dist/
rsync -avz /tmp/project_dist.tar.gz $SERVER:/tmp/Jungle(三个部分):
tar -czf /tmp/jungle_dist.tar.gz dist/
tar -czf /tmp/jungle_node_modules.tar.gz node_modules/
rsync -avz /tmp/jungle_dist.tar.gz $SERVER:/tmp/
rsync -avz /tmp/jungle_node_modules.tar.gz $SERVER:/tmp/
rsync -avz package.json $SERVER:/tmp/健康检查
Jungle 提供了 /health 端点用于健康检查:
// src/health/health.controller.ts
@Controller('health')
export class HealthController {
@Get()
check() {
return { status: 'ok', timestamp: new Date().toISOString() }
}
}部署脚本会验证:
- 容器状态(
docker inspect) - 容器内部健康检查(
wget http://localhost:3000/health) - 外部访问(
curl https://api.1px.club/health)
数据库迁移
Jungle 使用 TypeORM 管理数据库迁移:
# 生成迁移文件
pnpm run m:generate src/migrations/MigrationName
# 运行迁移(开发环境)
pnpm run m:run
# 运行迁移(生产环境,在容器内)
docker exec jungle_app pnpm run m:run:prod部署时自动运行迁移:
部署脚本会在重启容器后自动运行迁移:
# 重启容器
ssh $SERVER "cd $PROJECT_PATH && docker-compose up -d"
# 运行数据库迁移
ssh $SERVER "docker exec jungle_app pnpm run m:run:prod"敏感文件管理
Jungle 有两类敏感文件:
| 文件 | 说明 | 参考模板 |
|---|---|---|
.env.deploy | 服务器 IP、SSH 端口、路径等部署配置 | .env.deploy.example |
.env.production | 数据库密码、JWT 密钥等运行时配置 | .env.production(服务器) |
注意:.env.production 只存在于服务器上,不在本地创建,避免敏感信息泄露。
重构历程
Jungle 最初采用的是混合方案(本地构建 + GitHub Actions 部署),存在以下问题:
- ❌ Git 仓库污染:dist_prod.tar.gz 和 node_modules_prod.tar.gz 提交到版本控制
- ❌ 无版本管理:直接覆盖部署,无法回滚
- ❌ 缺少健康检查:部署后未验证服务可用性
- ❌ 无镜像缓存:每次可能重建镜像,浪费时间
- ⚠️ 依赖 GitHub Actions:消耗免费额度,受网络限制
重构后采用与前端项目一致的本地 CI/CD 方案,解决了所有问题。
最佳实践总结
通过 Blog、Vine、Bush、Earth、Jungle 等项目的实践,总结出以下最佳实践:
- 统一部署方案:前后端项目采用相同的部署架构,降低维护成本
- 多版本管理:始终保留历史版本,支持快速回滚
- 容器内构建:保证构建环境一致性,避免"在我机器上能跑"
- 智能缓存:基于文件 hash 检测变化,避免不必要的重建
- 精细化管理:配置变化才重建镜像,否则只重启容器
- 完整验证:部署前检测连通性,部署后验证服务可用性
- 敏感信息隔离:使用 .env 文件管理配置,不提交到 Git
- 自动化清理:定期清理旧版本,避免磁盘空间浪费
- 适配不同技术栈:根据项目特点(静态/SPA/SSR/后端)调整部署细节
- 持久化数据管理:数据库和上传文件跨版本共享
适用场景
本地 CI/CD 方案适合:
- ✅ 个人项目或小团队
- ✅ 资源受限场景(服务器配置低)
- ✅ 希望零云端成本
- ✅ 网络环境受限(GitHub Actions 不稳定)
- ✅ 多种技术栈混合部署(前端 + 后端)
不适合:
- ❌ 大型团队协作(缺少可观测性)
- ❌ 需要严格的权限控制
- ❌ 需要详细的部署审计日志