Skip to content

部署相关

掌握软件部署中常见的基础概念、部署方案与应用场景。

对象存储(阿里云 OSS/腾讯云 COS/亚马逊 AWS S3)

对象存储是一种云存储服务,专门用于存储非结构化数据,比如:文件、图片、视频等等。

  • 高扩展:只要你愿意给云厂商付费,存储容量无限
  • 高可用:数据会自动做冗余备份
  • 按需付费:不需要自己买服务器、磁盘,按存储容量、请求次数、流出流量等计费,无固定服务器成本
  • 唯一 URL:每个文件都有唯一的 URL,支持通过 HTTP/HTTPS 直接访问

CDN

CDN 是一组分布在全球各地的服务器,用于加速静态资源的访问。

工作原理:

  1. 用户请求一个文件,如:https://cdn.example.com/logo.png
  2. CDN 会根据用户的地理位置,将请求转发到离用户最近的服务器上
  3. 如果该服务器有缓存,直接返回
  4. 如果没有缓存,CDN 会存源站拉取后再返回,并缓存

源站:可以是在云厂商的 CDN 服务中配置我们自己的云服务器,也可以结合对象存储服务,从对象存储中拉取。

注意:CDN 本身不存储原始文件,它只是源站的 “缓存代理”。

优点:

  1. 速度快:用户会从最近的节点获取资源
  2. 减轻源站压力:大部分静态资源的请求都可以交给 CDN 节点处理,静态资源(图片、JS、CSS)不再占用源站带宽和算力,让源站专注处理动态业务
  3. 高可用:CDN 会在节点故障时自动切换其他节点
  4. 降低成本:通过 CDN 的缓存和流量调度,减少源站的带宽费用,尤其在全球访问场景下更明显
  5. 安全防护:CDN 可以提供 DDoS 防护、WAF 等安全能力,保护源站安全

WAF(Web Application Firewall):Web 应用防火墙,用于防护 Web 应用的安全,比如防止 SQL 注入、XSS 攻击等。

Nginx 配置对象存储 + CDN(前后端分离架构)

conf
# 前端项目构建后,将静态资源上传到对象存储
# 配置 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 为例:

  1. 当代码推送到远程仓库时,Github Actions 到调度器 Orchestrator 会检测事件(push),检查是否存在匹配事件的 Workflow ,把 Workflow 加入到代执行队列中
  2. 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 部署到我的云服务器中。

在下面的部署实践开始之前,你需要先了解一下我这边的限制条件:

  1. 我不希望产生对象存储/CDN 费用
  2. 我的云服务器资源非常非常非常有限,OOM 风险很高
  3. Github 私有仓库存在 Runner 资源限制(免费额度、免费存储限制),可以使用 self-hosted runner ,但依然需要考虑第 2 点
  4. 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 成功

相关命令

bash
# 开发
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

工作流程:

  1. 每次 pnpm run deploy:prodreleases/ 下创建带时间戳的新目录
  2. rsync 将构建产物上传到该目录
  3. ln -sfn 原子切换 current 软链接指向新目录(秒级生效,无中断)
  4. 自动清理最旧的版本,始终保留最近 3 个

回滚操作:

bash
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 健康检查

部署成功

相关命令

bash
# 开发
pnpm run c:up      # 启动本地开发环境(Docker)
pnpm run c:down    # 停止本地开发环境

# 部署
pnpm run deploy:prod  # 手动执行完整部署流程
pnpm run rollback     # 交互式回滚到历史版本

核心特性

与 Blog 保持一致:

  1. ✅ 多版本管理 + 软链接(秒级回滚)
  2. ✅ 容器内构建(环境一致性)
  3. ✅ 智能镜像缓存(基于 Dockerfile.dev 的 MD5 检测)
  4. ✅ 完整健康检查(SSH 预检 + HTTP 全链路验证)
  5. ✅ 敏感信息管理(.env.deploy)
  6. ✅ 自动清理机制(保留最近 3 个版本)
  7. ✅ 无需额外云端成本

Nginx 配置差异

Vine 的 Nginx 配置相比 Blog 更复杂,需要处理:

nginx
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 部署),存在以下问题:

  1. Git 仓库污染:dist.tar.gz 提交到版本控制,每次部署产生一个 commit
  2. 无版本管理:直接覆盖部署,无法回滚
  3. 缺少健康检查:部署后未验证服务可用性
  4. 无镜像缓存:每次可能重建镜像,浪费时间
  5. ⚠️ 依赖 GitHub Actions:消耗免费额度,受网络限制

重构后采用与 Blog 一致的本地 CI/CD 方案,解决了所有问题。

敏感文件管理

与 Blog 相同,Vine 也使用 .env.deploy 管理部署配置:

文件说明参考模板
.env.deploy服务器 IP、SSH 端口、路径等部署配置.env.deploy.example

配置示例:

bash
SERVER_IP=your_server_ip
SSH_PORT=2333
SERVER_USER=tangzhenming
PROJECT_PATH=/home/tangzhenming/project/vine

部署前准备

首次部署需要:

  1. 创建 .env.deploy 配置文件
  2. 配置 SSH 密钥(服务器公钥已加入 ~/.ssh/known_hosts
  3. 清理 Git 历史中的 dist.tar.gz(如果之前提交过)

清理 Git 历史:

bash
# 执行清理脚本
./scripts/cleanup-git-history.sh

# 强制推送到远程
git push origin --force --all

最佳实践总结

通过 Blog 和 Vine 两个项目的实践,总结出以下最佳实践:

  1. 统一部署方案:多个项目采用相同的部署架构,降低维护成本
  2. 多版本管理:始终保留历史版本,支持快速回滚
  3. 容器内构建:保证构建环境一致性,避免"在我机器上能跑"
  4. 智能缓存:基于文件 hash 检测变化,避免不必要的重建
  5. 完整验证:部署前检测连通性,部署后验证服务可用性
  6. 敏感信息隔离:使用 .env 文件管理配置,不提交到 Git
  7. 自动化清理:定期清理旧版本,避免磁盘空间浪费

适用场景

本地 CI/CD 方案适合:

  • ✅ 个人项目或小团队
  • ✅ 资源受限场景(服务器配置低)
  • ✅ 希望零云端成本
  • ✅ 网络环境受限(GitHub Actions 不稳定)

不适合:

  • ❌ 大型团队协作(缺少可观测性)
  • ❌ 需要严格的权限控制
  • ❌ 需要详细的部署审计日志

CI/CD 部署实践:vine-product

Vine Product 是 Vine 主应用的微前端子应用(Vue 3),同样采用本地 CI/CD 方案

与 Vine 主应用的差异

维度Vine 主应用Vine Product 子应用
框架React + ViteVue 3 + Vite
角色Qiankun 主应用(基座)Qiankun 子应用
部署方式独立容器独立容器
路由//product/
CI 流程Git Hooks(pre-commit、pre-push)Git Hooks(pre-commit、pre-push)
部署触发push 时自动询问push 时自动询问
代码检查ESLint + PrettierESLint + Prettier

部署架构

vine.1px.club/
├── /                    ← Vine 主应用(React)
│   └── 容器:vine_app
├── /product/            ← Vine Product 子应用(Vue)
│   └── 容器:vine_product_app
└── /api/                ← API 反向代理到 Jungle

通过 Traefik 网关统一路由:

yaml
# Traefik 路由优先级
vine-product: priority=100 # 子应用优先
vine: priority=1 # 主应用默认

微前端部署要点

  1. 独立部署,统一加载

    • 主应用和子应用各自独立容器
    • 各自独立的版本管理和回滚
    • 子应用通过 qiankun 动态加载,不支持独立访问
  2. Memory History 路由

    • 子应用使用 Memory History(路由状态由主应用管理)
    • 不支持直接访问子应用 URL(如 /product/finish
    • 必须通过主应用访问(主应用内部加载子应用)
  3. 同域部署

    • 通过 Traefik 统一域名
    • 无跨域问题,Cookie 可正常传递
    • 子应用入口:/product/(仅供 qiankun 加载)
  4. 健康检查策略

    • 容器状态:验证 Docker 容器运行正常
    • 入口可达:验证 qiankun 能获取入口 HTML
    • 静态资源:验证 assets 目录存在
    • 手动验证:部署后通过主应用验证子应用能否正常加载

部署命令

bash
# 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 健康检查

部署成功

相关命令

bash
# 开发
pnpm run c:up      # 启动本地开发环境(Docker)
pnpm run c:down    # 停止本地开发环境

# 部署
pnpm run deploy:prod  # 手动执行完整部署流程
pnpm run rollback     # 交互式回滚到历史版本

核心特性

与 Blog 保持一致:

  1. ✅ 多版本管理 + 软链接(秒级回滚)
  2. ✅ 容器内构建(环境一致性)
  3. ✅ 智能镜像缓存(基于 Dockerfile.dev 的 MD5 检测)
  4. ✅ 精细化容器管理(配置变化才重建,否则只重启)
  5. ✅ 完整健康检查(SSH 预检 + HTTP 全链路验证)
  6. ✅ 敏感信息管理(.env.deploy)
  7. ✅ 自动清理机制(保留最近 3 个版本)
  8. ✅ 无需额外云端成本

Nginx 配置差异

Bush 的 Nginx 配置相比 Blog 更复杂,需要处理:

nginx
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/UmiJSNext.js
渲染方式客户端渲染(CSR)服务端渲染(SSR)
服务器Nginx(静态文件服务)Node.js(运行时服务)
端口803000
构建产物dist/(纯静态文件).next/standalone/(含运行时)
启动命令nginxnode 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_hash

Next.js Standalone 模式

Earth 使用 Next.js 的 standalone 输出模式,生成最小化的运行时:

next.config.ts

typescript
const nextConfig: NextConfig = {
  output: 'standalone'
}

构建产物结构

.next/
├── standalone/              # 最小化运行时(包含 server.js)
│   ├── server.js           # 服务器入口
│   ├── node_modules/       # 运行时依赖(已优化)
│   └── .next/              # Next.js 运行时
├── static/                 # 静态资源(需要单独复制)
└── ...

部署要求

  1. 复制 standalone/ 目录的所有内容到服务器
  2. 复制 .next/static/ 到服务器的 .next/static/
  3. 复制 public/ 到服务器的 public/
  4. 运行 node server.js

工作流程

与其他项目保持一致:

开发阶段

git commit (CI)
    ├── lint-staged
    ├── ESLint
    ├── Prettier
    └── Commitlint

git push (CD)
    ├── 询问是否部署
    ├── SSH 连通性检测
    ├── Docker 容器内构建(生成 standalone)
    ├── Rsync 同步到服务器(standalone + static + public)
    ├── 切换软链接
    ├── 重启容器
    └── HTTP 健康检查(重试次数增加到 15 次)

部署成功

相关命令

bash
# 开发
pnpm run c:up      # 启动本地开发环境(Docker)
pnpm run c:down    # 停止本地开发环境

# 部署
pnpm run deploy:prod  # 手动执行完整部署流程
pnpm run rollback     # 交互式回滚到历史版本

核心特性

与其他项目保持一致:

  1. ✅ 多版本管理 + 软链接(秒级回滚)
  2. ✅ 容器内构建(环境一致性)
  3. ✅ 智能镜像缓存(基于 Dockerfile.dev 的 MD5 检测)
  4. ✅ 精细化容器管理(配置变化才重建,否则只重启)
  5. ✅ 完整健康检查(SSH 预检 + HTTP 全链路验证)
  6. ✅ 敏感信息管理(.env.deploy)
  7. ✅ 自动清理机制(保留最近 3 个版本)
  8. ✅ 无需额外云端成本

Dockerfile 差异

其他项目(Nginx)

dockerfile
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Earth(Node.js)

dockerfile
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:alpinenode:20-alpine
  • 端口:803000
  • 启动命令:nginxnode server.js
  • 不复制文件:构建产物通过 volume 挂载

docker-compose.yml 差异

其他项目

yaml
services:
  app:
    volumes:
      - ${RELEASE_DIR:-./current}:/usr/share/nginx/html # Nginx 静态文件目录
    labels:
      - 'traefik.http.services.app.loadbalancer.server.port=80'

Earth

yaml
services:
  earth:
    volumes:
      - ${RELEASE_DIR:-./current}:/app # Node.js 工作目录
    labels:
      - 'traefik.http.services.earth.loadbalancer.server.port=3000'

关键差异

  • 挂载目录:/usr/share/nginx/html/app
  • 服务端口:803000

部署脚本差异

构建产物上传

其他项目(单个 dist 目录):

bash
rsync -avz --delete dist/ $SERVER:$RELEASE_DIR/

Earth(三个目录):

bash
# 上传 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 次):

bash
MAX_RETRY=10

Earth(重试 15 次,Next.js 启动较慢):

bash
MAX_RETRY=15  # Next.js 启动较慢,增加重试次数

性能优化

构建优化

  • 使用 Docker Volume 缓存 node_modulespnpm 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 等项目的实践,总结出以下最佳实践:

  1. 统一部署方案:多个项目采用相同的部署架构,降低维护成本
  2. 多版本管理:始终保留历史版本,支持快速回滚
  3. 容器内构建:保证构建环境一致性,避免"在我机器上能跑"
  4. 智能缓存:基于文件 hash 检测变化,避免不必要的重建
  5. 精细化管理:配置变化才重建镜像,否则只重启容器
  6. 完整验证:部署前检测连通性,部署后验证服务可用性
  7. 敏感信息隔离:使用 .env 文件管理配置,不提交到 Git
  8. 自动化清理:定期清理旧版本,避免磁盘空间浪费
  9. 适配不同技术栈:根据项目特点(静态/SPA/SSR)调整部署细节

适用场景

本地 CI/CD 方案适合:

  • ✅ 个人项目或小团队
  • ✅ 资源受限场景(服务器配置低)
  • ✅ 希望零云端成本
  • ✅ 网络环境受限(GitHub Actions 不稳定)
  • ✅ 多种技术栈混合部署

不适合:

  • ❌ 大型团队协作(缺少可观测性)
  • ❌ 需要严格的权限控制
  • ❌ 需要详细的部署审计日志

CI/CD 部署实践:jungle

Jungle 是仓库作业管理系统的后端应用(NestJS),采用本地 CI/CD 方案,与前端项目保持架构一致。

技术栈特点

Jungle 作为后端应用,与前端项目有显著差异:

维度前端项目(Blog/Vine/Bush)Jungle 后端
框架VitePress/React/UmiJSNestJS
构建产物纯静态文件Node.js 运行时 + 依赖
服务器NginxNode.js
端口803000
启动命令nginxnode dist/main
依赖管理devDependenciesdependencies
环境变量构建时注入运行时注入
数据库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 健康检查

部署成功

相关命令

bash
# 开发
pnpm run c:up      # 启动本地开发环境(Docker)
pnpm run c:down    # 停止本地开发环境

# 部署
pnpm run deploy:prod  # 手动执行完整部署流程
pnpm run rollback     # 交互式回滚到历史版本

核心特性

与前端项目保持一致:

  1. ✅ 多版本管理 + 软链接(秒级回滚)
  2. ✅ 容器内构建(环境一致性)
  3. ✅ 智能镜像缓存(基于 Dockerfile.dev 的 MD5 检测)
  4. ✅ 精细化容器管理(配置变化才重建,否则只重启)
  5. ✅ 完整健康检查(SSH 预检 + HTTP 全链路验证)
  6. ✅ 敏感信息管理(.env.deploy + .env.production)
  7. ✅ 自动清理机制(保留最近 3 个版本)
  8. ✅ 无需额外云端成本

后端特有配置

1. 持久化数据管理

用户上传文件需要跨版本共享:

yaml
# docker-compose.yml
services:
  app:
    volumes:
      - ${RELEASE_DIR:-./current}:/app # 当前版本代码
      - ./uploads:/app/uploads # 上传文件(持久化)

2. 数据库连接

通过 Docker 网络连接数据库:

yaml
services:
  app:
    networks:
      - jungle_network
      - traefik_network

networks:
  jungle_network:
    external: true # 连接到 PostgreSQL + Redis
  traefik_network:
    external: true # 连接到 Traefik 网关

3. 环境变量管理

生产环境变量通过 .env.production 注入:

bash
# .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)

dockerfile
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Jungle(Node.js)

dockerfile
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"]

部署脚本差异

构建流程

前端项目(单次构建):

bash
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(两次安装):

bash
# 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"

上传内容

前端项目(单个目录):

bash
tar -czf /tmp/project_dist.tar.gz dist/
rsync -avz /tmp/project_dist.tar.gz $SERVER:/tmp/

Jungle(三个部分):

bash
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 端点用于健康检查:

typescript
// src/health/health.controller.ts
@Controller('health')
export class HealthController {
  @Get()
  check() {
    return { status: 'ok', timestamp: new Date().toISOString() }
  }
}

部署脚本会验证:

  1. 容器状态(docker inspect
  2. 容器内部健康检查(wget http://localhost:3000/health
  3. 外部访问(curl https://api.1px.club/health

数据库迁移

Jungle 使用 TypeORM 管理数据库迁移:

bash
# 生成迁移文件
pnpm run m:generate src/migrations/MigrationName

# 运行迁移(开发环境)
pnpm run m:run

# 运行迁移(生产环境,在容器内)
docker exec jungle_app pnpm run m:run:prod

部署时自动运行迁移

部署脚本会在重启容器后自动运行迁移:

bash
# 重启容器
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 部署),存在以下问题:

  1. Git 仓库污染:dist_prod.tar.gz 和 node_modules_prod.tar.gz 提交到版本控制
  2. 无版本管理:直接覆盖部署,无法回滚
  3. 缺少健康检查:部署后未验证服务可用性
  4. 无镜像缓存:每次可能重建镜像,浪费时间
  5. ⚠️ 依赖 GitHub Actions:消耗免费额度,受网络限制

重构后采用与前端项目一致的本地 CI/CD 方案,解决了所有问题。

最佳实践总结

通过 Blog、Vine、Bush、Earth、Jungle 等项目的实践,总结出以下最佳实践:

  1. 统一部署方案:前后端项目采用相同的部署架构,降低维护成本
  2. 多版本管理:始终保留历史版本,支持快速回滚
  3. 容器内构建:保证构建环境一致性,避免"在我机器上能跑"
  4. 智能缓存:基于文件 hash 检测变化,避免不必要的重建
  5. 精细化管理:配置变化才重建镜像,否则只重启容器
  6. 完整验证:部署前检测连通性,部署后验证服务可用性
  7. 敏感信息隔离:使用 .env 文件管理配置,不提交到 Git
  8. 自动化清理:定期清理旧版本,避免磁盘空间浪费
  9. 适配不同技术栈:根据项目特点(静态/SPA/SSR/后端)调整部署细节
  10. 持久化数据管理:数据库和上传文件跨版本共享

适用场景

本地 CI/CD 方案适合:

  • ✅ 个人项目或小团队
  • ✅ 资源受限场景(服务器配置低)
  • ✅ 希望零云端成本
  • ✅ 网络环境受限(GitHub Actions 不稳定)
  • ✅ 多种技术栈混合部署(前端 + 后端)

不适合:

  • ❌ 大型团队协作(缺少可观测性)
  • ❌ 需要严格的权限控制
  • ❌ 需要详细的部署审计日志

Released under the MIT License.