Skip to content

部署经验深度总结

基于多个项目(blog、bush、vine、vine-product、earth、jungle)的部署实践,深入总结应用部署的核心架构模式、关键技术点和最佳实践。

本文档聚焦于通用的部署架构和技术解析,具体项目的部署实践请参考 部署相关

一、核心架构模式

1.1 多版本管理 + 软链接架构

所有项目都采用了相同的版本管理架构:

$PROJECT_PATH/
├── releases/
│   ├── 20260220_143000/   ← 历史版本 1
│   ├── 20260220_183700/   ← 历史版本 2
│   └── 20260226_120000/   ← 当前版本
├── current -> releases/20260226_120000  (软链接)
├── Dockerfile
├── docker-compose.yml
├── nginx.conf
├── .dockerfile_hash       (配置变化检测)
└── .nginx_conf_hash       (配置变化检测)

核心优势:

  1. 秒级回滚:只需切换软链接 + 重启容器,无需重新构建
  2. 零停机部署:新版本上传完成后,原子切换软链接
  3. 版本追溯:保留最近 3 个版本,可快速定位问题
  4. 磁盘优化:自动清理旧版本,避免空间浪费

1.2 本地 CI/CD 流程

所有项目都采用基于 Git Hooks 的本地 CI/CD:

开发阶段

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. 询问是否部署(仅 main 分支)        │
│    2. SSH 连通性检测(快速失败)          │
│    3. Docker 容器内构建                  │
│    4. Rsync 增量同步到服务器             │
│    5. 切换软链接(原子操作)              │
│    6. 智能容器管理(重建/重启)           │
│    7. HTTP 健康验证(全链路)             │
└─────────────────────────────────────────┘

部署成功 + push 成功

关键特性:

  • 快速失败:SSH 连通性预检,避免构建完才发现传不上去
  • 环境一致:Docker 容器内构建,避免"在我机器上能跑"
  • 增量传输:rsync 只传输变化的文件,节省时间和带宽
  • 原子切换:软链接切换是原子操作,无中间状态
  • 智能管理:配置变化才重建镜像,否则只重启容器
  • 完整验证:部署后验证服务可达性,确保部署成功

1.3 智能镜像缓存机制

所有项目都实现了基于 MD5 的智能镜像缓存:

本地构建镜像缓存(Dockerfile.dev):

bash
# 计算 Dockerfile.dev 的 MD5
DOCKERFILE_HASH=$(md5 -q Dockerfile.dev)
HASH_FILE=".{project}_dev_image_hash"

# 检测是否需要重建
NEED_REBUILD=false
if ! docker images {project}-dev:latest -q | grep -q .; then
    NEED_REBUILD=true  # 镜像不存在
elif [ ! -f "$HASH_FILE" ] || [ "$(cat $HASH_FILE)" != "$DOCKERFILE_HASH" ]; then
    NEED_REBUILD=true  # Dockerfile 已变化
fi

# 只在需要时重建
if [ "$NEED_REBUILD" = true ]; then
    docker build -f Dockerfile.dev -t {project}-dev:latest .
    echo "$DOCKERFILE_HASH" > "$HASH_FILE"
fi

服务器镜像缓存(Dockerfile + nginx.conf):

bash
# 计算本地配置文件的 hash
LOCAL_DOCKERFILE_HASH=$(md5 -q Dockerfile)
LOCAL_NGINX_CONF_HASH=$(md5 -q nginx.conf)

# 获取服务器上的 hash
REMOTE_DOCKERFILE_HASH=$(ssh ... "cat $PROJECT_PATH/.dockerfile_hash 2>/dev/null || echo ''")
REMOTE_NGINX_CONF_HASH=$(ssh ... "cat $PROJECT_PATH/.nginx_conf_hash 2>/dev/null || echo ''")

# 判断是否需要重新构建
if [ "$LOCAL_DOCKERFILE_HASH" != "$REMOTE_DOCKERFILE_HASH" ] || \
   [ "$LOCAL_NGINX_CONF_HASH" != "$REMOTE_NGINX_CONF_HASH" ]; then
    # 重新构建并启动
    ssh ... "cd $PROJECT_PATH && docker-compose up -d --build"
    # 保存新的 hash
    ssh ... "echo '$LOCAL_DOCKERFILE_HASH' > $PROJECT_PATH/.dockerfile_hash"
    ssh ... "echo '$LOCAL_NGINX_CONF_HASH' > $PROJECT_PATH/.nginx_conf_hash"
else
    # 只重启容器(不重新构建)
    ssh ... "cd $PROJECT_PATH && docker-compose up -d --force-recreate --no-build"
fi

优势:

  • 避免不必要的镜像重建,节省时间
  • 配置变化时自动重建,确保一致性
  • 本地和服务器分别缓存,双重优化

二、项目类型对比分析

2.1 静态站点(Blog)

技术栈: VitePress

特点:

  • 纯静态 HTML/CSS/JS
  • 无需 API 交互
  • 构建产物:docs/.vitepress/dist/

Dockerfile:

dockerfile
FROM nginx:alpine
COPY nginx.htpasswd /etc/nginx/.htpasswd  # 简历页面访问控制
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Nginx 配置要点:

nginx
server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    # 简历访问控制
    location ~ ^/(about|resume) {
        auth_basic "Restricted Access";
        auth_basic_user_file /etc/nginx/.htpasswd;
        try_files $uri $uri.html $uri/ /index.html;
    }

    # 静态资源缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # SPA 路由
    location / {
        try_files $uri $uri/ /index.html;
    }
}

部署命令:

bash
pnpm run deploy:prod  # 构建 + 上传 + 部署
pnpm run rollback     # 回滚

2.2 SPA 应用(Bush)

技术栈: React + UmiJS + Ant Design Pro

特点:

  • 需要 API 反向代理
  • 需要处理用户上传文件
  • 构建产物:dist/

Dockerfile:

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

Nginx 配置要点:

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;
    }

    # 静态资源缓存(排除 /static/)
    location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
        if ($request_uri ~* "^/static/") {
          break;
        }
        expires 1y;
        add_header Cache-Control "public, max-age=31536000";
    }
}

docker-compose.yml 要点:

yaml
services:
  bush:
    volumes:
      - ${RELEASE_DIR:-./current}:/usr/share/nginx/html
      - ./logs:/var/log/nginx
      - /root/jungle_uploads:/app/shared_uploads # 共享上传目录

2.3 微前端主应用(Vine)

技术栈: React + Vite + Qiankun

特点:

  • 需要 API 反向代理
  • 需要加载微前端子应用
  • 需要处理子应用路由
  • 构建产物:dist/

Nginx 配置要点:

nginx
server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    # API 反向代理
    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;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # 微前端子应用路由(优先级高)
    location ^~ /product/ {
        try_files $uri $uri/ /product/index.html;
    }

    # 主应用路由
    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 on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}

docker-compose.yml 要点:

yaml
services:
  vine:
    labels:
      # 主应用路由(优先级低)
      - 'traefik.http.routers.vine.rule=Host(`vine.1px.club`)'
      - 'traefik.http.routers.vine.priority=1'

      # 子应用业务路由(优先级高,特殊处理)
      - 'traefik.http.routers.vine-product-business.rule=Host(`vine.1px.club`) && (Path(`/product/finish`) || PathPrefix(`/product/finish/`))'
      - 'traefik.http.routers.vine-product-business.priority=200'
      - 'traefik.http.routers.vine-product-business.service=vine'

2.4 微前端子应用(Vine Product)

技术栈: Vue 3 + Vite + Qiankun

特点:

  • 使用 Memory History(不支持独立访问)
  • 仅供主应用加载
  • 需要 CORS 支持
  • 构建产物:dist/

Nginx 配置要点:

nginx
server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    # 子应用入口(仅供 qiankun 加载)
    location = /product/ {
        try_files /index.html =404;
    }

    location = /product {
        return 301 /product/;
    }

    # 静态资源(需要 CORS)
    location ^~ /product/assets/ {
        rewrite ^/product/(.*)$ /$1 break;
        try_files $uri =404;
        expires 1y;
        add_header Cache-Control "public, immutable";
        add_header Access-Control-Allow-Origin "https://vine.1px.club";
    }

    # 其他路径返回 404(Memory History 不需要)
    location ^~ /product/ {
        return 404 '{"error": "Not Found", "message": "This micro-frontend app uses Memory History and can only be accessed through the main application"}';
        add_header Content-Type application/json;
    }

    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}

docker-compose.yml 要点:

yaml
services:
  vine_product:
    labels:
      # 子应用路由(优先级中等,仅匹配资源路径)
      - 'traefik.http.routers.vine-product.rule=Host(`vine.1px.club`) && (Path(`/product`) || Path(`/product/`) || PathPrefix(`/product/assets/`))'
      - 'traefik.http.routers.vine-product.priority=100'

      # 自动添加尾部斜杠
      - 'traefik.http.middlewares.vine-product-slash.redirectregex.regex=^(https?://[^/]+)/product$'
      - 'traefik.http.middlewares.vine-product-slash.redirectregex.replacement=$1/product/'
      - 'traefik.http.middlewares.vine-product-slash.redirectregex.permanent=true'

健康检查策略:

bash
# 1. 容器状态检查
CONTAINER_STATUS=$(ssh ... "docker ps --filter name=vine_product_app --format '{{.Status}}'")

# 2. 入口 HTML 可达性检查
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://vine.1px.club/product/)

# 3. 静态资源目录检查
ASSETS_CHECK=$(ssh ... "ls -la $PROJECT_PATH/current/assets/ | wc -l")

三、关键技术点深度解析

3.1 Docker 容器内构建

为什么要在容器内构建?

  1. 环境一致性:避免"在我机器上能跑"的问题
  2. 依赖隔离:不污染宿主机环境
  3. 可复现性:任何人在任何机器上都能得到相同的构建结果

实现方式:

bash
# 1. 构建开发环境镜像(带缓存检测)
DOCKERFILE_HASH=$(md5 -q Dockerfile.dev)
HASH_FILE=".{project}_dev_image_hash"

if [ ! -f "$HASH_FILE" ] || [ "$(cat $HASH_FILE)" != "$DOCKERFILE_HASH" ]; then
    docker build -f Dockerfile.dev -t {project}-dev:latest .
    echo "$DOCKERFILE_HASH" > "$HASH_FILE"
fi

# 2. 在临时容器内执行构建
docker run --rm \
    --name {project}-builder-temp \
    -e CI=true \
    -e HUSKY=0 \
    -e PNPM_HOME=/tmp/pnpm \
    -v "$(pwd):/app" \
    -v {project}_node_modules:/app/node_modules \
    -v {project}_pnpm_store:/tmp/pnpm \
    {project}-dev:latest \
    sh -c "pnpm config set store-dir /tmp/pnpm && pnpm install --frozen-lockfile && pnpm run build"

# 3. 验证构建产物
if [ ! -d "dist" ] || [ -z "$(ls -A dist)" ]; then
    echo "❌ dist/ 目录为空或不存在"
    exit 1
fi

关键参数说明:

  • --rm:容器退出后自动删除
  • -e CI=true:告诉构建工具这是 CI 环境
  • -e HUSKY=0:禁用 Git Hooks
  • -v "$(pwd):/app":挂载项目目录
  • -v {project}_node_modules:/app/node_modules:使用 Docker Volume 缓存依赖
  • -v {project}_pnpm_store:/tmp/pnpm:使用 Docker Volume 缓存 pnpm store

优势:

  • 构建速度快(利用 Docker Volume 缓存)
  • 环境干净(不污染宿主机)
  • 可复现(任何人都能得到相同结果)

3.2 Rsync 增量同步

为什么使用 Rsync?

  1. 增量传输:只传输变化的文件,节省时间和带宽
  2. 断点续传:网络中断后可以继续传输
  3. 压缩传输:减少网络流量
  4. 保留权限:保留文件权限和时间戳

实现方式:

bash
# 上传配置文件
rsync -avz -e "ssh -p $SSH_PORT" \
    Dockerfile docker-compose.yml nginx.conf \
    $SERVER_USER@$SERVER_IP:$PROJECT_PATH/

# 上传构建产物到版本目录
rsync -avz --delete -e "ssh -p $SSH_PORT" \
    dist/ \
    $SERVER_USER@$SERVER_IP:$RELEASE_DIR/

参数说明:

  • -a:归档模式,保留权限、时间戳等
  • -v:详细输出
  • -z:压缩传输
  • --delete:删除目标目录中多余的文件(保持同步)
  • -e "ssh -p $SSH_PORT":指定 SSH 端口

注意事项:

  • dist/ 后面的斜杠很重要,表示同步目录内容而不是目录本身
  • --delete 确保目标目录与源目录完全一致

3.3 软链接原子切换

为什么使用软链接?

  1. 原子操作ln -sfn 是原子操作,无中间状态
  2. 秒级切换:切换软链接只需几毫秒
  3. 零停机:容器重启时间极短(< 1 秒)
  4. 易于回滚:只需切换软链接指向

实现方式:

bash
# 切换 current 软链接指向新版本
ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP \
    "ln -sfn $RELEASE_DIR $PROJECT_PATH/current"

# 重启容器以应用新版本
ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP \
    "cd $PROJECT_PATH && docker-compose restart"

参数说明:

  • -s:创建符号链接(软链接)
  • -f:强制覆盖已存在的链接
  • -n:将目标视为普通文件(避免递归链接)

工作原理:

# 部署前
current -> releases/20260220_143000

# 上传新版本
releases/20260226_120000/  ← 新版本已上传

# 切换软链接(原子操作)
current -> releases/20260226_120000

# 重启容器
docker-compose restart  ← 容器重新挂载 current 目录

3.4 智能容器管理

为什么需要智能管理?

  1. 避免不必要的重建:配置未变化时,只需重启容器
  2. 节省时间:重启容器比重建镜像快得多
  3. 保持一致性:配置变化时自动重建

实现方式:

bash
# 1. 计算本地配置文件的 hash
LOCAL_DOCKERFILE_HASH=$(md5 -q Dockerfile)
LOCAL_NGINX_CONF_HASH=$(md5 -q nginx.conf)

# 2. 获取服务器上的 hash
REMOTE_DOCKERFILE_HASH=$(ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP \
    "cat $PROJECT_PATH/.dockerfile_hash 2>/dev/null || echo ''")
REMOTE_NGINX_CONF_HASH=$(ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP \
    "cat $PROJECT_PATH/.nginx_conf_hash 2>/dev/null || echo ''")

# 3. 判断是否需要重新构建
if [ "$LOCAL_DOCKERFILE_HASH" != "$REMOTE_DOCKERFILE_HASH" ] || \
   [ "$LOCAL_NGINX_CONF_HASH" != "$REMOTE_NGINX_CONF_HASH" ]; then
    echo "   检测到配置文件变化, 需要重新构建镜像..."

    # 重新构建并启动
    ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP \
        "cd $PROJECT_PATH && $RELEASE_DIR_ENV docker-compose up -d --build"

    # 保存新的 hash
    ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP \
        "echo '$LOCAL_DOCKERFILE_HASH' > $PROJECT_PATH/.dockerfile_hash && \
         echo '$LOCAL_NGINX_CONF_HASH' > $PROJECT_PATH/.nginx_conf_hash"
else
    echo "   配置文件未变化, 重新创建容器以更新挂载..."

    # 只重启容器(不重新构建)
    ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP \
        "cd $PROJECT_PATH && $RELEASE_DIR_ENV docker-compose up -d --force-recreate --no-build"
fi

关键命令说明:

  • docker-compose up -d --build:重新构建镜像并启动
  • docker-compose up -d --force-recreate --no-build:强制重新创建容器但不重建镜像

优势:

  • 配置变化时自动重建镜像
  • 配置未变化时只重启容器,节省时间
  • 通过 hash 文件持久化状态

3.5 完整健康检查

为什么需要健康检查?

  1. 快速失败:部署前检测 SSH 连通性,避免构建完才发现传不上去
  2. 验证部署:部署后验证服务可达性,确保部署成功
  3. 及时发现问题:通过 HTTP 请求验证全链路(Traefik → Nginx → 静态文件)

实现方式:

bash
# 1. 部署前:SSH 连通性检测(快速失败)
echo "🔗 检测 SSH 连通性..."
if ! ssh -p $SSH_PORT -o ConnectTimeout=5 -o BatchMode=yes \
    $SERVER_USER@$SERVER_IP exit 2>/dev/null; then
    echo "❌ SSH 连接失败, 请检查:"
    echo "   1. ~/.ssh/ 目录下是否有对应服务器的私钥"
    echo "   2. 是否已执行 ssh-add (key 有 passphrase 时需要)"
    echo "   3. 服务器公钥是否已加入 ~/.ssh/known_hosts"
    exit 1
fi
echo "✅ SSH 连接正常"

# 2. 部署后:HTTP 健康验证(全链路)
echo "🔍 验证服务可达性..."
MAX_RETRY=10
for i in $(seq 1 $MAX_RETRY); do
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
        --max-time 5 https://{domain} || echo "000")
    if [ "$HTTP_CODE" = "200" ]; then
        echo "✅ 服务验证通过 (HTTP $HTTP_CODE)"
        break
    fi
    if [ $i -eq $MAX_RETRY ]; then
        echo "❌ 服务验证失败 (HTTP $HTTP_CODE), 请手动检查"
        echo "   可能原因: 容器启动中、Nginx 配置错误、Traefik 路由异常"
        exit 1
    fi
    echo "   等待中... [$i/$MAX_RETRY] HTTP $HTTP_CODE"
    sleep 3
done

关键参数说明:

  • -o ConnectTimeout=5:SSH 连接超时 5 秒
  • -o BatchMode=yes:非交互模式,避免密码提示
  • --max-time 5:HTTP 请求超时 5 秒
  • -s:静默模式,不输出进度
  • -o /dev/null:丢弃响应内容
  • -w "%{http_code}":只输出 HTTP 状态码

优势:

  • 部署前快速失败,避免浪费时间
  • 部署后验证全链路,确保服务可用
  • 自动重试机制,容忍短暂的启动延迟

四、最佳实践总结

4.1 部署架构设计

实践说明优势
多版本管理使用时间戳命名版本目录可追溯、易回滚
软链接切换通过软链接指向当前版本原子操作、秒级切换
自动清理保留最近 3 个版本节省磁盘空间
容器内构建在 Docker 容器内执行构建环境一致、可复现
增量同步使用 rsync 增量传输节省时间和带宽

4.2 性能优化

实践说明优势
智能镜像缓存基于 MD5 检测配置变化避免不必要的重建
Docker Volume 缓存缓存 node_modules 和 pnpm store加速构建
精细化容器管理配置变化才重建,否则只重启节省时间
静态资源缓存设置长期缓存(1 年)减少服务器负载
Gzip 压缩启用 Nginx gzip 压缩减少传输大小

4.3 可靠性保障

实践说明优势
快速失败部署前检测 SSH 连通性避免浪费时间
完整验证部署后验证服务可达性确保部署成功
自动重试HTTP 健康检查自动重试容忍短暂延迟
错误处理set -e 确保命令失败时终止避免错误传播
日志输出详细的部署日志便于排查问题

4.4 安全性

实践说明优势
敏感信息隔离使用 .env.deploy 管理配置不提交到 Git
SSH 密钥认证使用 SSH 密钥而非密码更安全
HTTP Basic Auth简历页面访问控制保护隐私
HTTPS通过 Traefik 自动配置 HTTPS加密传输
安全头通过 Traefik 中间件添加安全头防止常见攻击

4.5 开发体验

实践说明优势
Git Hooks自动化 CI/CD 流程无需手动操作
交互式部署部署前询问确认避免误操作
交互式回滚选择版本回滚操作简单
详细提示清晰的错误提示和建议便于排查问题
统一命令所有项目使用相同命令降低学习成本

五、常见问题与解决方案

5.1 构建相关

问题 1:构建失败 - 内存不足

Error: JavaScript heap out of memory

解决方案:

  • 在 Docker 容器内构建(已实现)
  • 增加 Docker 容器内存限制
  • 使用 NODE_OPTIONS=--max-old-space-size=4096 增加 Node.js 内存

问题 2:依赖安装失败

ERR_PNPM_FETCH_404  GET https://registry.npmjs.org/xxx: Not Found

解决方案:

  • 使用国内镜像源(已配置)
  • 清理 pnpm store:pnpm store prune
  • 删除 pnpm-lock.yaml 重新安装

5.2 部署相关

问题 3:SSH 连接失败

❌ SSH 连接失败

解决方案:

  1. 检查 SSH 密钥是否存在:ls ~/.ssh/
  2. 检查 SSH 密钥是否已添加:ssh-add -l
  3. 添加 SSH 密钥:ssh-add ~/.ssh/id_rsa
  4. 检查服务器公钥:ssh-keyscan -p 2333 server_ip >> ~/.ssh/known_hosts

问题 4:Rsync 传输失败

rsync: connection unexpectedly closed

解决方案:

  • 检查网络连接
  • 检查服务器磁盘空间:df -h
  • 检查服务器权限:ls -la $PROJECT_PATH

问题 5:容器启动失败

Error response from daemon: driver failed programming external connectivity

解决方案:

  • 检查端口是否被占用:netstat -tulpn | grep 80
  • 重启 Docker:systemctl restart docker
  • 检查 docker-compose.yml 配置

5.3 运行时相关

问题 6:服务验证失败 - HTTP 404

❌ 服务验证失败 (HTTP 404)

解决方案:

  1. 检查 Nginx 配置:docker exec {container} cat /etc/nginx/conf.d/default.conf
  2. 检查静态文件:docker exec {container} ls -la /usr/share/nginx/html
  3. 检查 Nginx 日志:docker logs {container}
  4. 检查 Traefik 路由:访问 Traefik Dashboard

问题 7:API 请求失败 - 502 Bad Gateway

502 Bad Gateway

解决方案:

  1. 检查后端服务是否运行:docker ps | grep jungle
  2. 检查网络连接:docker network inspect traefik_network
  3. 检查 Nginx 配置中的 upstream
  4. 检查后端服务日志:docker logs jungle_app

问题 8:静态资源 404

GET /static/xxx.png 404

解决方案:

  1. 检查上传目录挂载:docker inspect {container} | grep Mounts
  2. 检查文件是否存在:ls -la /root/jungle_uploads
  3. 检查 Nginx 配置中的 alias 路径
  4. 检查文件权限:chmod -R 755 /root/jungle_uploads

六、前后端部署差异对比

6.1 技术栈对比

维度前端项目(静态/SPA)后端项目(API 服务)
构建产物纯静态文件Node.js 运行时 + 依赖
服务器NginxNode.js
端口803000
启动命令nginxnode dist/main
依赖管理devDependenciesdependencies
环境变量构建时注入运行时注入
数据库PostgreSQL + Redis
持久化数据数据库 Volume + 上传文件
构建位置本地 Docker 容器本地 Docker 容器

6.2 部署架构对比

前端项目架构:

$PROJECT_PATH/
├── releases/
│   └── 20260226_183700/
│       └── dist/          # 静态文件
├── current -> releases/20260226_183700
├── Dockerfile             # Nginx 镜像
├── docker-compose.yml
└── nginx.conf

后端项目架构:

$PROJECT_PATH/
├── releases/
│   └── 20260226_183700/
│       ├── dist/          # 编译产物
│       ├── node_modules/  # 生产依赖
│       └── package.json
├── current -> releases/20260226_183700
├── uploads/               # 持久化数据(跨版本共享)
├── Dockerfile             # Node.js 镜像
├── docker-compose.yml
└── .env.production        # 运行时环境变量

七、后端部署关键差异点

7.1 后端特有配置

7.1.1 持久化数据管理

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

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

7.1.2 数据库连接

通过 Docker 网络连接数据库:

yaml
services:
  app:
    networks:
      - jungle_network
      - traefik_network

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

7.1.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=***

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

7.3 部署脚本差异

7.3.1 构建流程

前端项目(单次构建):

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"

7.3.2 上传内容

前端项目(单个目录):

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/

7.4 健康检查

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

7.5 数据库迁移

后端应用通常需要管理数据库 schema 变更:

迁移工具选择:

  • TypeORM:适合 NestJS 项目
  • Prisma:现代化 ORM,类型安全
  • Knex.js:灵活的 SQL 查询构建器
  • Sequelize:传统 ORM

部署时自动运行迁移:

bash
# 重启容器
ssh $SERVER "cd $PROJECT_PATH && docker-compose up -d"

# 运行数据库迁移
ssh $SERVER "docker exec app_container pnpm run migration:run"

注意事项:

  • 迁移应该是幂等的(可重复执行)
  • 生产环境迁移前应先备份数据库
  • 考虑使用蓝绿部署避免停机

八、部署方案选择指南

8.1 本地构建 vs 服务器构建

在设计后端应用部署方案时,需要对比两种主要方案:

维度本地构建(已采用)⭐服务器构建
上传内容dist + node_modules源码
上传大小~200MB(首次)
~几MB(增量)
~10MB
构建位置本地服务器
构建稳定性✅ 高(本地可控)⚠️ 中(受服务器网络影响)
网络依赖本地网络服务器网络(国内受限)
调试难度✅ 低(本地调试)⚠️ 高(需 SSH 到服务器)
服务器压力✅ 低⚠️ 中

为什么选择本地构建?

考虑到服务器在国内,面临以下网络问题:

  1. npm/pnpm 依赖下载不稳定(即使配置了国内镜像)
  2. 某些包的 postinstall 脚本需要访问 GitHub
  3. Docker 基础镜像拉取可能较慢

本地构建的优势:

  • ✅ 本地构建环境可控,可以科学上网
  • ✅ 构建失败不影响服务器运行
  • ✅ Rsync 增量同步,实际传输量小
  • ✅ 与前端项目架构完全一致

8.2 核心文件设计

8.2.1 目录结构

jungle/
├── scripts/
│   ├── deploy.sh           # 部署脚本(本地执行)
│   └── rollback.sh         # 回滚脚本(本地执行)
├── Dockerfile              # 生产环境 Dockerfile
├── Dockerfile.dev          # 开发环境 Dockerfile
├── docker-compose.yml      # 生产环境 docker-compose
├── docker-compose.dev.yml  # 开发环境 docker-compose
├── .env.deploy             # 部署配置(不提交)
├── .env.production         # 生产环境变量(不提交)
└── .env.development        # 开发环境变量

8.2.2 Dockerfile(生产环境)

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

8.2.3 docker-compose.yml

yaml
version: '3.8'

services:
  app:
    container_name: jungle_app
    build:
      context: .
      dockerfile: Dockerfile
    env_file:
      - .env.production
    environment:
      - NODE_ENV=production
    volumes:
      - ${RELEASE_DIR:-./current}:/app # 挂载当前版本目录
      - /root/jungle_uploads:/app/uploads
    depends_on:
      - postgres
      - redis
    restart: always
    networks:
      - traefik_network
      - jungle_internal
    labels:
      - 'traefik.enable=true'
      - 'traefik.http.routers.jungle.rule=Host(`api.1px.club`)'
      - 'traefik.http.routers.jungle.entrypoints=web,websecure'
      - 'traefik.http.routers.jungle.tls.certresolver=letsencrypt'
      - 'traefik.http.routers.jungle.middlewares=security-headers@file'
      - 'traefik.http.services.jungle.loadbalancer.server.port=3000'

  postgres:
    container_name: jungle_postgres
    networks:
      - jungle_internal
    image: postgres:17-alpine
    env_file:
      - .env.production
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: always

  redis:
    container_name: jungle_redis
    networks:
      - jungle_internal
    image: redis:latest
    env_file:
      - .env.production
    volumes:
      - redis_data:/data
    restart: always

volumes:
  postgres_data:
  redis_data:

networks:
  traefik_network:
    external: true
  jungle_internal:
    driver: bridge

8.2.4 scripts/deploy.sh(本地执行)

bash
#!/bin/bash
# 本地构建 + 上传 + 重启

set -e  # 命令失败则终止

# 加载部署配置
ENV_FILE="$(dirname "$0")/../.env.deploy"
if [ ! -f "$ENV_FILE" ]; then
    echo "❌ 未找到 .env.deploy, 请复制 .env.deploy.example 并填入真实值"
    exit 1
fi
source "$ENV_FILE"

# 确保工作目录是项目根目录
cd "$(dirname "$0")/.."

echo "🚀 开始部署 Jungle..."

# ==============================================================================
# 0. 检测 SSH 连通性 (快速失败, 避免构建完才发现传不上去)
echo "🔗 检测 SSH 连通性..."
if ! ssh -p $SSH_PORT -o ConnectTimeout=5 -o BatchMode=yes \
    $SERVER_USER@$SERVER_IP exit 2>/dev/null; then
    echo "❌ SSH 连接失败, 请检查:"
    echo "   1. ~/.ssh/ 目录下是否有对应服务器的私钥"
    echo "   2. 是否已执行 ssh-add (key 有 passphrase 时需要)"
    echo "   3. 服务器公钥是否已加入 ~/.ssh/known_hosts"
    exit 1
fi
echo "✅ SSH 连接正常"

# ==============================================================================
# 1. 在临时容器内构建 (保证环境一致)
echo "🔨 执行构建 (Docker 临时容器内)..."

# 清理旧的 dist 和 node_modules 目录
if [ -d "dist" ]; then
    echo "   清理旧的 dist/ 目录..."
    rm -rf dist
fi
if [ -d "node_modules" ] && [ ! -L "node_modules" ]; then
    echo "   清理旧的 node_modules/ 目录..."
    rm -rf node_modules
fi

# 构建开发环境镜像 (基于 Dockerfile.dev hash 检测, 文件有变更时自动重建)
DOCKERFILE_HASH=$(md5 -q Dockerfile.dev)
HASH_FILE=".jungle_dev_image_hash"

NEED_REBUILD=false
if ! docker images jungle-dev:latest -q | grep -q .; then
    NEED_REBUILD=true
elif [ ! -f "$HASH_FILE" ] || [ "$(cat $HASH_FILE)" != "$DOCKERFILE_HASH" ]; then
    echo "   Dockerfile.dev 已更改, 重新构建镜像..."
    NEED_REBUILD=true
fi

if [ "$NEED_REBUILD" = true ]; then
    echo "   构建开发环境镜像..."
    docker build -f Dockerfile.dev -t jungle-dev:latest .
    echo "$DOCKERFILE_HASH" > "$HASH_FILE"
fi

# 在容器内执行构建
echo "   在容器内执行 pnpm build..."
docker run --rm \
    --name jungle-builder-temp \
    -e CI=true \
    -e HUSKY=0 \
    -e PNPM_HOME=/tmp/pnpm \
    -v "$(pwd):/app" \
    -v jungle_node_modules:/app/node_modules \
    -v jungle_pnpm_store:/tmp/pnpm \
    jungle-dev:latest \
    sh -c "pnpm config set store-dir /tmp/pnpm && \
           pnpm install --frozen-lockfile && \
           pnpm run build && \
           rm -rf node_modules && \
           pnpm install --prod --frozen-lockfile"

# 验证 dist 目录
if [ ! -d "dist" ] || [ -z "$(ls -A dist)" ]; then
    echo "❌ dist/ 目录为空或不存在"
    exit 1
fi

# 验证 node_modules 目录
if [ ! -d "node_modules" ] || [ -z "$(ls -A node_modules)" ]; then
    echo "❌ node_modules/ 目录为空或不存在"
    exit 1
fi

echo "✅ 容器内构建成功"
echo "   dist/ 大小: $(du -sh dist | cut -f1)"
echo "   node_modules/ 大小: $(du -sh node_modules | cut -f1)"

# ==============================================================================
# 2. 上传到服务器
echo "📤 上传到服务器..."

# 生成本次部署的版本目录 (时间戳命名)
RELEASE_DIR="$PROJECT_PATH/releases/$(date +%Y%m%d_%H%M%S)"

# 确保远程目录结构存在
ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP "mkdir -p $PROJECT_PATH/releases"

# 上传配置文件 (Dockerfile、docker-compose.yml)
rsync -avz -e "ssh -p $SSH_PORT" \
    Dockerfile docker-compose.yml \
    $SERVER_USER@$SERVER_IP:$PROJECT_PATH/

# 上传构建产物到本次版本目录
echo "   上传 dist/..."
rsync -avz --delete -e "ssh -p $SSH_PORT" \
    dist/ \
    $SERVER_USER@$SERVER_IP:$RELEASE_DIR/dist/

echo "   上传 node_modules/..."
rsync -avz --delete -e "ssh -p $SSH_PORT" \
    node_modules/ \
    $SERVER_USER@$SERVER_IP:$RELEASE_DIR/node_modules/

echo "   上传 package.json..."
rsync -avz -e "ssh -p $SSH_PORT" \
    package.json \
    $SERVER_USER@$SERVER_IP:$RELEASE_DIR/

# 切换 current 软链接指向新版本 (原子操作, 秒级生效)
ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP \
    "ln -sfn $RELEASE_DIR $PROJECT_PATH/current"

echo "✅ 上传完成, 当前版本: $(basename $RELEASE_DIR)"

# 清理本地构建产物
rm -rf dist node_modules

# 自动清理旧版本, 保留最近 3 个
echo "🧹 清理旧版本 (保留最近 3 个)..."
ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP \
    "ls -dt $PROJECT_PATH/releases/*/ 2>/dev/null | tail -n +4 | xargs -r rm -rf"

# ==============================================================================
# 3. 启动/重启容器
echo "🔄 启动/重启容器..."

# 设置环境变量, 指定当前版本目录
export RELEASE_DIR_ENV="RELEASE_DIR=$RELEASE_DIR"

# 检测是否需要重新构建镜像 (Dockerfile 变化)
NEED_REBUILD=false

# 计算本地配置文件的 hash
LOCAL_DOCKERFILE_HASH=$(md5 -q Dockerfile)

# 获取服务器上的 hash (如果存在)
REMOTE_DOCKERFILE_HASH=$(ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP \
    "cat $PROJECT_PATH/.dockerfile_hash 2>/dev/null || echo ''")

# 判断是否需要重新构建
if [ "$LOCAL_DOCKERFILE_HASH" != "$REMOTE_DOCKERFILE_HASH" ]; then
    echo "   检测到配置文件变化, 需要重新构建镜像..."
    NEED_REBUILD=true
fi

if [ "$NEED_REBUILD" = true ]; then
    # 重新构建并启动
    ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP "cd $PROJECT_PATH && $RELEASE_DIR_ENV docker-compose up -d --build"

    # 保存新的 hash
    ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP \
        "echo '$LOCAL_DOCKERFILE_HASH' > $PROJECT_PATH/.dockerfile_hash"

    echo "   ✅ 镜像重新构建完成"
else
    # 只重启容器 (不重新构建)
    echo "   配置文件未变化, 重新创建容器以更新挂载..."
    ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP "cd $PROJECT_PATH && $RELEASE_DIR_ENV docker-compose up -d --force-recreate --no-build"

    echo "   ✅ 容器重新创建完成"
fi

# ==============================================================================
# 4. 验证服务可达性 (全链路: Traefik → Jungle → 健康检查)
echo "🔍 验证服务可达性..."
MAX_RETRY=15
for i in $(seq 1 $MAX_RETRY); do
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
        --max-time 5 https://api.1px.club/health || echo "000")
    if [ "$HTTP_CODE" = "200" ]; then
        echo "✅ 服务验证通过 (HTTP $HTTP_CODE)"
        break
    fi
    if [ $i -eq $MAX_RETRY ]; then
        echo "❌ 服务验证失败 (HTTP $HTTP_CODE), 请手动检查"
        echo "   可能原因: 容器启动中、应用启动失败、Traefik 路由异常"
        exit 1
    fi
    echo "   等待中... [$i/$MAX_RETRY] HTTP $HTTP_CODE"
    sleep 3
done

echo "✅ 部署完成!"
echo "🌐 访问: https://api.1px.club"

8.2.5 scripts/rollback.sh(本地执行)

bash
#!/bin/bash
set -e

# 加载部署配置
ENV_FILE="$(dirname "$0")/../.env.deploy"
if [ ! -f "$ENV_FILE" ]; then
    echo "❌ 未找到 .env.deploy"
    exit 1
fi
source "$ENV_FILE"

cd "$(dirname "$0")/.."

# 获取服务器上的版本列表
RELEASES=$(ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP \
    "ls -dt $PROJECT_PATH/releases/*/ 2>/dev/null | xargs -I{} basename {}" 2>/dev/null)

if [ -z "$RELEASES" ]; then
    echo "❌ 服务器上没有任何历史版本"
    exit 1
fi

# 获取当前版本
CURRENT=$(ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP \
    "basename \$(readlink $PROJECT_PATH/current 2>/dev/null)" 2>/dev/null || echo "")

echo ""
echo "📋 可用历史版本(从新到旧):"
echo ""

i=1
while IFS= read -r release; do
    if [ "$release" = "$CURRENT" ]; then
        echo "  $i) $release  ← 当前版本"
    else
        echo "  $i) $release"
    fi
    i=$((i + 1))
done <<< "$RELEASES"

echo ""
read -p "请输入要回滚到的版本编号(直接回车取消): " choice

if [ -z "$choice" ]; then
    echo "已取消"
    exit 0
fi

TARGET=$(echo "$RELEASES" | sed -n "${choice}p")

if [ -z "$TARGET" ]; then
    echo "❌ 无效的编号:$choice"
    exit 1
fi

if [ "$TARGET" = "$CURRENT" ]; then
    echo "⚠️  所选版本已是当前版本"
    exit 0
fi

echo ""
echo "🔄 回滚到版本: $TARGET"
ssh -p $SSH_PORT $SERVER_USER@$SERVER_IP \
    "ln -sfn $PROJECT_PATH/releases/$TARGET $PROJECT_PATH/current \
     && docker-compose -f $PROJECT_PATH/docker-compose.yml restart"

echo ""
echo "✅ 回滚完成!"
echo "   当前版本:$TARGET"
echo "🌐 访问:https://api.1px.club"

8.2.6 .env.deploy.example

bash
# 部署配置(复制为 .env.deploy 后填入真实值)
SERVER_IP=your_server_ip
SSH_PORT=2333
SERVER_USER=tangzhenming
PROJECT_PATH=/home/tangzhenming/project/jungle

8.2.7 部署流程对比

重构前(混合方案):

本地机器                          GitHub                    服务器
   │                                │                         │
   ├─ 1. 容器内构建                  │                         │
   ├─ 2. 生成 tar.gz                │                         │
   ├─ 3. git commit                 │                         │
   ├─ 4. git push ──────────────────>│                         │
   │                                ├─ 5. GitHub Actions      │
   │                                ├─ 6. SSH 到服务器 ───────>│
   │                                │                         ├─ 7. git pull
   │                                │                         ├─ 8. 解压 tar.gz
   │                                │                         ├─ 9. docker-compose up
   │                                │                         └─ 10. 完成

重构后(本地构建):

本地机器                                                      服务器
   │                                                            │
   ├─ 1. SSH 连通性检测                                         │
   ├─ 2. Docker 容器内构建                                      │
   ├─ 3. Rsync 增量同步 ─────────────────────────────────────>│
   │      - dist/                                              ├─ 4. 接收构建产物
   │      - node_modules/                                      ├─ 5. 切换软链接
   │      - package.json                                       ├─ 6. 重启容器
   ├─ 7. HTTP 健康检查 <─────────────────────────────────────┤
   └─ 8. 完成                                                  └─ 完成

优势:

  1. ✅ 无需 GitHub Actions(节省免费额度)
  2. ✅ 无需提交构建产物到 Git(仓库干净)
  3. ✅ 支持多版本管理和回滚
  4. ✅ 与前端项目架构一致
  5. ✅ 本地构建环境可控(可科学上网)
  6. ✅ Rsync 增量同步(实际传输量小)

九、总结

9.1 核心原则

通过对多个项目的深入分析,我们总结出了一套成熟的部署架构:

  1. 多版本管理:使用时间戳命名版本目录,保留最近 3 个版本
  2. 软链接切换:通过软链接实现原子切换,支持秒级回滚
  3. 容器内构建:保证构建环境一致性,避免"在我机器上能跑"
  4. 智能缓存:基于 MD5 检测配置变化,避免不必要的重建
  5. 完整验证:部署前检测连通性,部署后验证服务可用性

9.2 适用场景

本地 CI/CD 方案适合:

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

不适合:

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

9.3 最佳实践总结

类别实践说明优势
架构多版本管理使用时间戳命名版本目录可追溯、易回滚
软链接切换通过软链接指向当前版本原子操作、秒级
自动清理保留最近 3 个版本节省磁盘空间
构建容器内构建在 Docker 容器内执行构建环境一致、可复现
增量同步使用 rsync 增量传输节省时间和带宽
性能智能镜像缓存基于 MD5 检测配置变化避免不必要的重建
Docker Volume 缓存缓存 node_modules 和 pnpm store加速构建
精细化容器管理配置变化才重建,否则只重启节省时间
可靠性快速失败部署前检测 SSH 连通性避免浪费时间
完整验证部署后验证服务可达性确保部署成功
自动重试HTTP 健康检查自动重试容忍短暂延迟
安全敏感信息隔离使用 .env 文件管理配置不提交到 Git
SSH 密钥认证使用 SSH 密钥而非密码更安全
HTTPS通过 Traefik 自动配置 HTTPS加密传输
体验Git Hooks自动化 CI/CD 流程无需手动操作
交互式部署部署前询问确认避免误操作
交互式回滚选择版本回滚操作简单
统一命令所有项目使用相同命令降低学习成本

9.4 项目类型对比

项目类型框架示例服务器关键配置
静态站点VitePressNginx静态资源缓存
SPAReact + UmiJSNginxAPI 代理 + SPA 路由
微前端React + QiankunNginx子应用路由 + CORS
SSRNext.jsNode.jsStandalone 模式
后端 APINestJSNode.js数据库 + 持久化数据

9.5 技术选型建议

前端项目:

  • 静态站点 → VitePress + Nginx
  • SPA 应用 → React/Vue + Vite + Nginx
  • SSR 应用 → Next.js + Node.js
  • 微前端 → Qiankun + Nginx

后端项目:

  • RESTful API → NestJS + Node.js
  • 数据库 → PostgreSQL + Redis
  • 文件存储 → 本地 Volume(小规模)/ 对象存储(大规模)

部署方案:

  • 个人项目 → 本地 CI/CD(Git Hooks)
  • 团队项目 → 云端 CI/CD(GitHub Actions / GitLab CI)
  • 混合部署 → 根据项目特点选择

参考资料:

Released under the MIT License.