部署经验深度总结
基于多个项目(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 (配置变化检测)核心优势:
- 秒级回滚:只需切换软链接 + 重启容器,无需重新构建
- 零停机部署:新版本上传完成后,原子切换软链接
- 版本追溯:保留最近 3 个版本,可快速定位问题
- 磁盘优化:自动清理旧版本,避免空间浪费
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):
# 计算 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):
# 计算本地配置文件的 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:
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 配置要点:
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;
}
}部署命令:
pnpm run deploy:prod # 构建 + 上传 + 部署
pnpm run rollback # 回滚2.2 SPA 应用(Bush)
技术栈: React + UmiJS + Ant Design Pro
特点:
- 需要 API 反向代理
- 需要处理用户上传文件
- 构建产物:
dist/
Dockerfile:
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]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 要点:
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 配置要点:
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 要点:
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 配置要点:
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 要点:
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'健康检查策略:
# 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. 构建开发环境镜像(带缓存检测)
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?
- 增量传输:只传输变化的文件,节省时间和带宽
- 断点续传:网络中断后可以继续传输
- 压缩传输:减少网络流量
- 保留权限:保留文件权限和时间戳
实现方式:
# 上传配置文件
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 软链接原子切换
为什么使用软链接?
- 原子操作:
ln -sfn是原子操作,无中间状态 - 秒级切换:切换软链接只需几毫秒
- 零停机:容器重启时间极短(< 1 秒)
- 易于回滚:只需切换软链接指向
实现方式:
# 切换 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. 计算本地配置文件的 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 完整健康检查
为什么需要健康检查?
- 快速失败:部署前检测 SSH 连通性,避免构建完才发现传不上去
- 验证部署:部署后验证服务可达性,确保部署成功
- 及时发现问题:通过 HTTP 请求验证全链路(Traefik → Nginx → 静态文件)
实现方式:
# 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 连接失败解决方案:
- 检查 SSH 密钥是否存在:
ls ~/.ssh/ - 检查 SSH 密钥是否已添加:
ssh-add -l - 添加 SSH 密钥:
ssh-add ~/.ssh/id_rsa - 检查服务器公钥:
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)解决方案:
- 检查 Nginx 配置:
docker exec {container} cat /etc/nginx/conf.d/default.conf - 检查静态文件:
docker exec {container} ls -la /usr/share/nginx/html - 检查 Nginx 日志:
docker logs {container} - 检查 Traefik 路由:访问 Traefik Dashboard
问题 7:API 请求失败 - 502 Bad Gateway
502 Bad Gateway解决方案:
- 检查后端服务是否运行:
docker ps | grep jungle - 检查网络连接:
docker network inspect traefik_network - 检查 Nginx 配置中的 upstream
- 检查后端服务日志:
docker logs jungle_app
问题 8:静态资源 404
GET /static/xxx.png 404解决方案:
- 检查上传目录挂载:
docker inspect {container} | grep Mounts - 检查文件是否存在:
ls -la /root/jungle_uploads - 检查 Nginx 配置中的 alias 路径
- 检查文件权限:
chmod -R 755 /root/jungle_uploads
六、前后端部署差异对比
6.1 技术栈对比
| 维度 | 前端项目(静态/SPA) | 后端项目(API 服务) |
|---|---|---|
| 构建产物 | 纯静态文件 | Node.js 运行时 + 依赖 |
| 服务器 | Nginx | Node.js |
| 端口 | 80 | 3000 |
| 启动命令 | nginx | node dist/main |
| 依赖管理 | devDependencies | dependencies |
| 环境变量 | 构建时注入 | 运行时注入 |
| 数据库 | 无 | 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 持久化数据管理
用户上传文件需要跨版本共享:
# docker-compose.yml
services:
app:
volumes:
- ${RELEASE_DIR:-./current}:/app # 当前版本代码
- ./uploads:/app/uploads # 上传文件(持久化)7.1.2 数据库连接
通过 Docker 网络连接数据库:
services:
app:
networks:
- jungle_network
- traefik_network
networks:
jungle_network:
external: true # 连接到 PostgreSQL + Redis
traefik_network:
external: true # 连接到 Traefik 网关7.1.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=***7.2 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"]7.3 部署脚本差异
7.3.1 构建流程
前端项目(单次构建):
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"7.3.2 上传内容
前端项目(单个目录):
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/7.4 健康检查
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)
7.5 数据库迁移
后端应用通常需要管理数据库 schema 变更:
迁移工具选择:
- TypeORM:适合 NestJS 项目
- Prisma:现代化 ORM,类型安全
- Knex.js:灵活的 SQL 查询构建器
- Sequelize:传统 ORM
部署时自动运行迁移:
# 重启容器
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 到服务器) |
| 服务器压力 | ✅ 低 | ⚠️ 中 |
为什么选择本地构建?
考虑到服务器在国内,面临以下网络问题:
- npm/pnpm 依赖下载不稳定(即使配置了国内镜像)
- 某些包的 postinstall 脚本需要访问 GitHub
- 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(生产环境)
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
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: bridge8.2.4 scripts/deploy.sh(本地执行)
#!/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(本地执行)
#!/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
# 部署配置(复制为 .env.deploy 后填入真实值)
SERVER_IP=your_server_ip
SSH_PORT=2333
SERVER_USER=tangzhenming
PROJECT_PATH=/home/tangzhenming/project/jungle8.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. 完成 └─ 完成优势:
- ✅ 无需 GitHub Actions(节省免费额度)
- ✅ 无需提交构建产物到 Git(仓库干净)
- ✅ 支持多版本管理和回滚
- ✅ 与前端项目架构一致
- ✅ 本地构建环境可控(可科学上网)
- ✅ Rsync 增量同步(实际传输量小)
九、总结
9.1 核心原则
通过对多个项目的深入分析,我们总结出了一套成熟的部署架构:
- 多版本管理:使用时间戳命名版本目录,保留最近 3 个版本
- 软链接切换:通过软链接实现原子切换,支持秒级回滚
- 容器内构建:保证构建环境一致性,避免"在我机器上能跑"
- 智能缓存:基于 MD5 检测配置变化,避免不必要的重建
- 完整验证:部署前检测连通性,部署后验证服务可用性
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 项目类型对比
| 项目类型 | 框架示例 | 服务器 | 关键配置 |
|---|---|---|---|
| 静态站点 | VitePress | Nginx | 静态资源缓存 |
| SPA | React + UmiJS | Nginx | API 代理 + SPA 路由 |
| 微前端 | React + Qiankun | Nginx | 子应用路由 + CORS |
| SSR | Next.js | Node.js | Standalone 模式 |
| 后端 API | NestJS | Node.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)
- 混合部署 → 根据项目特点选择
参考资料:
- 部署相关 - 具体项目的部署实践
- Docker 官方文档
- Nginx 官方文档
- Traefik 官方文档