Nuxt: 项目部署

最后更新:2026-08-26

MegaShop 开发完成,Charlie 要把它部署到生产环境。这不是简单的 npm run build——需要 Docker 编排全套服务、CI/CD 自动化流水线、运行时监控和错误追踪、完整的上线检查清单。一次部署,长期稳定运行。

1. 你将学到


2. 一个架构师的真实故事

(1) 痛点:上线即崩溃

Charlie 第一次部署 MegaShop——忘记跑数据库迁移、环境变量漏配、Redis 没启动、SSL 证书过期。上线 5 分钟后 Alice 就发现 500 错误,Bob 手动回滚花了 1 小时。

(2) 生产级部署体系的解法

完整的部署体系:自动化 CI/CD + Docker Compose + 监控 + 上线检查清单,每一步都有验证,出错自动回滚。

(3) 收益:自动化 + 可观测

推送代码后全自动部署,Sentry 实时捕获错误,Prometheus 监控性能指标,Alice 再也不会遇到未发现的 500 错误。


3. 生产构建优化

(1) 构建产物分析

▶ 示例:Build 分析与优化

BASH
# Build for production
npm run build

# Analyze bundle
npx nuxi analyze

输出:

TEXT 📖 仅展示
# 命令执行成功

▶ 示例:环境变量安全检查

TYPESCRIPT
// server/utils/env-check.ts
export default defineEventHandler((event) => {
  if (getRequestURL(event).pathname !== '/api/health') return

  const config = useRuntimeConfig()
  const required = ['databaseUrl', 'jwtAccessSecret', 'jwtRefreshSecret']
  const missing = required.filter(key => !config[key as keyof typeof config])

  if (missing.length > 0 && process.env.NODE_ENV === 'production') {
    console.error(`Missing required env vars: ${missing.join(', ')}`)
  }

  return {
    status: missing.length === 0 ? 'ok' : 'degraded',
    timestamp: Date.now(),
    version: process.env.APP_VERSION || 'unknown'
  }
})

输出:

TEXT 📖 仅展示
// 执行成功

(2) 构建优化检查清单

检查项 要求 验证方法
JS Bundle gzip < 200KB nuxi analyze
CSS 提取 ✅ 独立文件 构建产物检查
图片 WebP ✅ 自动转换 NuxtImg 验证
环境变量 无硬编码密钥 grep 检查
Tree Shaking 无未使用代码 bundle 分析
Source Map 生产关闭 nuxt.config.ts

4. Docker Compose 编排

(1) 生产部署流程

100%
flowchart TB
    A[Git Push] --> B[GitHub Actions CI]
    B --> C{Tests Pass?}
    C -->|No| D[Notify Team + Block]
    C -->|Yes| E[Build Docker Image]
    E --> F[Push to Registry]
    F --> G[Deploy to Staging]
    G --> H{Smoke Test?}
    H -->|No| I[Rollback Staging]
    H -->|Yes| J[Deploy to Production]
    J --> K{Health Check?}
    K -->|No| L[Auto Rollback]
    K -->|Yes| M[Done ✅]

▶ 示例:生产级 Docker Compose

YAML
# docker-compose.prod.yml
version: '3.8'

services:
  web:
    image: ghcr.io/megashop/megashop:latest
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://megashop:${DB_PASSWORD}@db:5432/megashop
      - REDIS_URL=redis://redis:6379
      - JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET}
      - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}
      - DEPLOY_TARGET=node-server
    depends_on:
      db: { condition: service_healthy }
      redis: { condition: service_started }
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    restart: unless-stopped
    deploy:
      resources:
        limits: { memory: 1G, cpus: '1.0' }

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=megashop
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=megashop
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U megashop"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.prod.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - web
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

输出:

TEXT 📖 仅展示
CONTAINER ID   IMAGE          STATUS         PORTS
abc123         nginx:latest   Up 2 hours     0.0.0.0:80->80/tcp

5. CI/CD 流水线

▶ 示例:完整的生产部署工作流

YAML
# .github/workflows/deploy-production.yml
name: Deploy Production

on:
  release:
    types: [published]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: megashop/megashop

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env: { POSTGRES_USER: test, POSTGRES_PASSWORD: test, POSTGRES_DB: megashop_test }
        ports: ['5432:5432']
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      - run: npx prisma migrate deploy
        env: { DATABASE_URL: postgresql://test:test@localhost:5432/megashop_test }
      - run: npm run lint
      - run: npm run typecheck
      - run: npm run test:coverage

  build-push:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GHCR_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

  deploy:
    needs: build-push
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /opt/megashop
            docker pull ghcr.io/megashop/megashop:latest
            docker compose -f docker-compose.prod.yml up -d --no-build
            docker compose exec web npx prisma migrate deploy
            echo "Deploy completed at $(date)"

      - name: Health check
        run: |
          for i in {1..15}; do
            if curl -sf https://megashop.com/api/health; then
              echo "Health check passed"
              exit 0
            fi
            echo "Waiting for health check... ($i/15)"
            sleep 5
          done
          echo "Health check failed!"
          exit 1

      - name: Rollback on failure
        if: failure()
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /opt/megashop
            docker compose down
            docker tag ghcr.io/megashop/megashop:previous ghcr.io/megashop/megashop:latest
            docker compose -f docker-compose.prod.yml up -d --no-build
            echo "Rollback completed at $(date)"

输出:

TEXT 📖 仅展示
CONTAINER ID   IMAGE          STATUS         PORTS
abc123         nginx:latest   Up 2 hours     0.0.0.0:80->80/tcp

6. 监控与日志

▶ 示例:Sentry 错误追踪

TYPESCRIPT
// plugins/sentry.client.ts
import * as Sentry from '@sentry/vue'

export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig()

  Sentry.init({
    dsn: config.public.sentryDsn,
    environment: config.public.environment,
    release: config.public.version,
    integrations: [
      new Sentry.BrowserTracing({
        routingInstrumentation: Sentry.vueRouterInstrumentation(nuxtApp.$router)
      })
    ],
    tracesSampleRate: 0.1,
    replaysSessionSampleRate: 0.1,
    replaysOnErrorSampleRate: 1.0
  })

  return {
    provide: {
      sentry: Sentry
    }
  }
})

输出:

TEXT 📖 仅展示
// 执行成功

▶ 示例:Server 端日志

TYPESCRIPT
// server/middleware/logging.ts
export default defineEventHandler((event) => {
  const start = Date.now()
  const method = getMethod(event)
  const url = getRequestURL(event)

  event.node.res.on('finish', () => {
    const duration = Date.now() - start
    const status = event.node.res.statusCode

    // Structured log
    const logEntry = {
      method, url: url.pathname,
      status, duration,
      userId: event.context.user?.id || null,
      timestamp: new Date().toISOString()
    }

    if (status >= 500) {
      console.error(JSON.stringify(logEntry))
    } else if (duration > 1000) {
      console.warn(JSON.stringify({ ...logEntry, alert: 'slow_request' }))
    } else {
      console.log(JSON.stringify(logEntry))
    }
  })
})

输出:

TEXT 📖 仅展示
// 执行成功

▶ 示例:健康检查 API

TYPESCRIPT
// server/api/health.get.ts
export default defineEventHandler(async () => {
  const checks: Record<string, string> = {}

  // Check database
  try {
    await prisma.$queryRaw`SELECT 1`
    checks.database = 'ok'
  } catch {
    checks.database = 'error'
  }

  // Check Redis
  try {
    const storage = useStorage('products')
    await storage.setItem('health-check', 'ok', { ttl: 10 })
    checks.redis = 'ok'
  } catch {
    checks.redis = 'error'
  }

  const allOk = Object.values(checks).every(v => v === 'ok')

  setHeader(useEvent(), 'cache-control', 'no-store')

  return {
    status: allOk ? 'ok' : 'degraded',
    checks,
    version: process.env.APP_VERSION || 'unknown',
    uptime: process.uptime(),
    timestamp: new Date().toISOString()
  }
})

输出:

TEXT 📖 仅展示
// 执行成功

7. 上线检查清单

(1) 生产上线检查

# 检查项 操作 验证方法
1 SSL 证书 配置 Let's Encrypt curl -v https://megashop.com
2 环境变量 所有密钥已配置 /api/health 检查
3 数据库迁移 prisma migrate deploy 查询商品验证
4 Redis 连接 启动并连接 /api/health 检查
5 CDN 配置 Cloudflare/CloudFront 静态资源缓存命中
6 缓存预热 热门商品页首次访问 curl 首页 + 列表页
7 健康检查 /api/health 返回 ok 自动化验证
8 Sentry 初始化 错误追踪可用 触发测试错误
9 Sitemap 生成 /sitemap.xml 可访问 Google Search Console
10 回滚预案 旧镜像可用 记录 rollback 步骤

(2) 回滚预案

步骤 操作 命令
1 停止当前服务 docker compose down
2 恢复旧镜像 docker tag xxx:previous xxx:latest
3 启动旧版本 docker compose up -d --no-build
4 验证健康 curl /api/health
5 通知团队 Slack/Email 通知

8. 综合示例:MegaShop 部署命令

BASH
# ============================================
# MegaShop Production Deployment
# Complete deployment + monitoring setup
# ============================================

# 1. Set up server (first time only)
ssh prod-server
mkdir -p /opt/megashop/ssl
# Copy SSL certificates
# Copy .env with production secrets

# 2. Initial deployment
cd /opt/megashop
git clone https://github.com/megashop/megashop.git .
docker compose -f docker-compose.prod.yml up -d

# 3. Database setup
docker compose exec web npx prisma migrate deploy
docker compose exec web npx prisma db seed

# 4. Verify
curl -f https://megashop.com/api/health
curl -f https://megashop.com/sitemap.xml

# 5. Cache warmup
curl https://megashop.com/
curl https://megashop.com/products
curl https://megashop.com/products/1

# 6. Monitor
docker compose logs -f web
# Check Sentry dashboard
# Check health endpoint periodically

❓ 常见问题

Q Docker 镜像怎么更新?
A CI/CD 自动 build + push 新镜像到 GHCR,服务器 docker pull + docker compose up -d --no-build。零停机用蓝绿部署。
Q Sentry 免费额度够用吗?
A 免费版每月 5 thousand 错误事件。MegaShop 初期足够,后期按需升级。开发错误不计入(只在生产上报)。
Q 健康检查接口需要鉴权吗?
A 不需要。/api/health 必须公开,用于负载均衡器、K8s、Docker healthcheck 探测。但不要暴露敏感信息。
Q 缓存预热怎么做?
A 部署后用脚本访问热门页面(首页/列表页/Top 100 商品详情),触发 ISR 缓存生成。也可在 seed 后自动执行。
Q 日志怎么收集?
A Docker 日志用 docker compose logs。生产环境推荐 ELK(Elasticsearch + Logstash + Kibana)或 Loki + Grafana。MegaShop 初期用结构化 console.log + Sentry。
Q 数据库迁移失败怎么办?
A Prisma migrate deploy 只应用已验证的迁移文件。如果迁移失败,修复迁移文件后重新部署。生产永远不用 prisma migrate reset(会丢数据)。

📖 小节


📝 作业

  1. 基础题(难度⭐):将 MegaShop 部署到生产环境(Docker Compose),验证 /api/health 返回 ok
  2. 进阶题(难度⭐⭐):配置 GitHub Actions CI/CD,实现 push main 自动部署,包含健康检查和自动回滚
  3. 挑战题(难度⭐⭐⭐):完成完整的运维体系——Sentry 错误追踪 + 结构化日志 + Prometheus 指标 + 上线检查清单自动化验证

---|

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏