概述

CI/CD(持续集成/持续部署)是现代 Web 开发的标准实践。对于 Nuxt 应用而言,由于涉及服务端渲染、预渲染、环境变量等多个环节,部署流程相对复杂。本文记录了 Nuxt 应用的 CI/CD 部署实践。

部署架构

流程概览

开发者推送代码到 GitHub
         │
         ▼
┌─────────────────────────────┐
│     GitHub Actions 触发     │
│  ┌─────────────────────────┐│
│  │ 1. Checkout 代码        ││
│  │ 2. Setup Node.js        ││
│  │ 3. npm ci               ││
│  │ 4. 构建项目             ││
│  │ 5. 打包构建产物         ││
│  └─────────────────────────┘│
└─────────────────────────────┘
         │
         ▼
┌─────────────────────────────┐
│     SCP 上传到服务器        │
└─────────────────────────────┘
         │
         ▼
┌─────────────────────────────┐
│     SSH 远程执行部署        │
│  ┌─────────────────────────┐│
│  │ 1. 清理旧构建产物       ││
│  │ 2. 解压新构建产物       ││
│  │ 3. PM2 重启服务         ││
│  └─────────────────────────┘│
└─────────────────────────────┘
         │
         ▼
     部署完成

服务器架构

                    ┌─────────────┐
    用户请求 ──────►│   Nginx     │
                    │  (端口 80)  │
                    └──────┬──────┘
                           │ 反向代理
                           ▼
                    ┌─────────────┐
                    │   Nuxt      │
                    │  (端口 3000)│
                    │  PM2 集群   │
                    └─────────────┘

GitHub Actions 配置

官方文档:https://docs.github.com/en/actions

工作流文件

创建 .github/workflows/deploy.yml

name: Deploy

on:
  push:
    branches:
      - main
  workflow_dispatch:  # 支持手动触发

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
      # 1. 检出代码
      - name: Checkout
        uses: actions/checkout@v4

      # 2. 设置 Node.js 环境
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      # 3. 安装依赖
      - name: Install dependencies
        run: |
          npm ci
          npm install --platform=linux --arch=x64 sharp

      # 4. 安装 jemalloc(可选,解决 sharp 内存问题)
      - name: Install jemalloc
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y -qq libjemalloc2 > /dev/null 2>&1

      # 5. 构建项目
      - name: Build Project
        run: npm run build
        env:
          NODE_ENV: production
          NODE_OPTIONS: '--max-old-space-size=6144 --no-node-snapshot'
          VIPS_WARNING: '0'
          LD_PRELOAD: /usr/lib/x86_64-linux-gnu/libjemalloc.so.2

      # 6. 打包构建产物
      - name: Zip Output
        run: tar -czf release.tar.gz .output ecosystem.config.cjs

      # 7. 上传到服务器
      - name: Upload to Server
        uses: appleboy/scp-action@v0.1.4
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          source: 'release.tar.gz'
          target: '/tmp'

      # 8. 执行部署
      - name: Execute Deploy
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd /opt/app
            rm -rf .output
            tar -xzf /tmp/release.tar.gz -C /opt/app
            rm /tmp/release.tar.gz
            
            pm2 delete app 2>/dev/null || true
            if [ -f /opt/app/.env ]; then
              set -a; source /opt/app/.env; set +a
            fi
            pm2 start ecosystem.config.cjs
            pm2 save

关键配置说明

跨平台依赖安装

- name: Install dependencies
  run: |
    npm ci
    npm install --platform=linux --arch=x64 sharp

CI 环境是 Linux,但开发环境可能是 Windows/macOS。npm ci 会根据当前平台安装依赖,需要额外安装 Linux 平台的 sharp 原生二进制。

内存优化

NODE_OPTIONS: '--max-old-space-size=6144 --no-node-snapshot'
  • --max-old-space-size=6144:将 Node.js 堆内存上限设为 6GB
  • --no-node-snapshot:禁用 V8 快照,减少内存占用

jemalloc

sharp 依赖的 libvips 在某些环境下可能与 glibc 默认的内存分配器冲突,导致 munmap_chunk(): invalid pointer 崩溃。使用 jemalloc 可以解决这个问题:

LD_PRELOAD: /usr/lib/x86_64-linux-gnu/libjemalloc.so.2

GitHub Secrets 配置

必需的 Secrets

在 GitHub 仓库的 Settings → Secrets and variables → Actions 中配置:

Secret 名称说明
SERVER_HOST服务器 IP 地址
SERVER_USERSSH 登录用户名
SERVER_SSH_KEYSSH 私钥完整内容
STUDIO_GITHUB_CLIENT_IDGitHub OAuth Client ID(如使用 Nuxt Studio)
STUDIO_GITHUB_CLIENT_SECRETGitHub OAuth Client Secret

SSH 密钥配置

  1. 生成 SSH 密钥对:
ssh-keygen -t ed25519 -C "github-actions" -f github-actions
  1. 将公钥添加到服务器:
ssh-copy-id -i github-actions.pub user@your-server
  1. 将私钥内容添加到 GitHub Secrets 的 SERVER_SSH_KEY

GitHub OAuth App 配置

如需使用 Nuxt Studio,需要配置 GitHub OAuth:

  1. 前往 GitHub Developer Settings → OAuth Apps → New OAuth App
  2. 填写信息:

    • Homepage URL: https://your-domain.com
    • Authorization callback URL: https://your-domain.com/__nuxt_studio/auth/github
  3. 创建后获取 Client ID 和 Client Secret

PM2 配置

官方文档:https://pm2.keymetrics.io

ecosystem.config.cjs

module.exports = {
  apps: [
    {
      name: 'app',
      port: '3000',
      exec_mode: 'cluster',
      instances: 'max',
      script: './.output/server/index.mjs',
      max_memory_restart: '512M',
      env: {
        NODE_ENV: 'production',
        NITRO_PRESET: 'node-server',
        NUXT_PUBLIC_SITE_URL: 'https://example.com'
      }
    }
  ]
}

配置项说明

配置项说明
exec_modecluster集群模式,利用多核 CPU
instancesmax启动与 CPU 核心数相同的进程
max_memory_restart512M单进程内存超限自动重启
script.output/server/index.mjsNuxt 服务端入口

环境变量注意事项

运行时不要设置 NUXT_STUDIO 环境变量。Nuxt 会将所有 NUXT_ 前缀的环境变量映射到 runtimeConfig。如果设置 NUXT_STUDIO='true',会把 runtimeConfig.studio 从对象覆盖为字符串,导致运行时报错。

NUXT_STUDIO 仅在构建时用于条件判断是否加载 nuxt-studio 模块。

服务器环境配置

目录结构

/opt/app/
├── .output/                # Nuxt 构建产物(CI 部署时覆盖)
│   └── server/
│       └── index.mjs       # 服务端入口
├── ecosystem.config.cjs    # PM2 配置(CI 部署时覆盖)
└── .env                    # 运行时凭据(手动创建,不被覆盖)

服务器 .env 文件

# /opt/app/.env

# Nuxt Studio GitHub OAuth(如使用)
STUDIO_GITHUB_CLIENT_ID=your_client_id
STUDIO_GITHUB_CLIENT_SECRET=your_client_secret
STUDIO_GITHUB_REDIRECT_URL=https://example.com/__nuxt_studio/auth/github

.env 文件不会被 CI 部署覆盖,首次部署时需要手动创建。

Node.js 环境安装

推荐使用 nvm 管理 Node.js 版本:

# 安装 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.bashrc

# 安装 Node.js 22
nvm install 22
nvm use 22
nvm alias default 22

# 安装 PM2
npm install -g pm2

PM2 常用命令

# 启动服务
pm2 start ecosystem.config.cjs

# 查看状态
pm2 status

# 查看日志
pm2 logs app

# 实时监控
pm2 monit

# 重启服务
pm2 restart app

# 停止服务
pm2 stop app

# 保存当前进程列表(开机自启)
pm2 save

# 生成开机自启脚本
pm2 startup

Nginx 配置

反向代理配置

# /etc/nginx/sites-available/app
server {
    listen 80;
    server_name example.com www.example.com;

    # 重定向到 HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    # SSL 证书
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # SSL 配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    # 反向代理到 Nuxt
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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_cache_bypass $http_upgrade;
    }

    # 静态资源缓存
    location /_nuxt/ {
        proxy_pass http://localhost:3000;
        proxy_cache static_cache;
        proxy_cache_valid 200 365d;
        proxy_cache_key $uri;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    # IPX 图片缓存
    location /_ipx/ {
        proxy_pass http://localhost:3000;
        proxy_cache image_cache;
        proxy_cache_valid 200 7d;
        add_header Cache-Control "public, max-age=604800";
    }
}

启用配置

# 创建软链接
sudo ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/

# 测试配置
sudo nginx -t

# 重载 Nginx
sudo systemctl reload nginx

Nginx 官方文档:https://nginx.org/en/docs

环境变量管理

构建时 vs 运行时

阶段变量来源用途
构建时GitHub Actions env条件加载模块、注入凭据
运行时服务器 .env 文件OAuth 认证、API 密钥

环境变量前缀约定

前缀处理方式示例
NUXT_自动映射到 runtimeConfigNUXT_PUBLIC_SITE_URL
STUDIO_nuxt-studio 模块直接读取STUDIO_GITHUB_CLIENT_ID
NITRO_Nitro 引擎配置NITRO_PRESET

安全建议

  1. 敏感信息不入库:OAuth 凭据、API 密钥等通过 Secrets 或 .env 管理
  2. 最小权限原则:OAuth App 只请求必要的 scope
  3. 定期轮换:定期更新密钥和令牌
  4. 审计日志:监控异常登录和操作

故障排查

构建失败:OOM

症状:JavaScript heap out of memory

解决:增加 Node.js 堆内存

NODE_OPTIONS: '--max-old-space-size=6144'

构建失败:munmap_chunk

症状:munmap_chunk(): invalid pointer

解决:安装并使用 jemalloc

- name: Install jemalloc
  run: sudo apt-get install -y libjemalloc2

- name: Build Project
  env:
    LD_PRELOAD: /usr/lib/x86_64-linux-gnu/libjemalloc.so.2

PM2 进程异常重启

排查命令:

# 查看进程状态
pm2 status

# 查看日志
pm2 logs app --lines 100

# 查看内存使用
pm2 monit

如果频繁因内存超限重启,调整 max_memory_restart 值。

部署后页面 404

排查步骤:

  1. 确认 .output/ 目录已正确解压
  2. 确认 PM2 进程正在运行
  3. 检查 Nginx 配置是否正确

高级配置

多环境部署

# .github/workflows/deploy.yml
jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    environment: staging
    steps:
      # ... 部署到测试环境

  deploy-production:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      # ... 部署到生产环境

部署通知

- name: Notify on Success
  if: success()
  uses: 8398a7/action-slack@v3
  with:
    status: success
    webhook_url: ${{ secrets.SLACK_WEBHOOK }}

- name: Notify on Failure
  if: failure()
  uses: 8398a7/action-slack@v3
  with:
    status: failure
    webhook_url: ${{ secrets.SLACK_WEBHOOK }}

部署检查清单

首次部署

  • 服务器安装 Node.js 22+
  • 服务器安装 PM2
  • 配置 GitHub Secrets
  • 创建服务器 .env 文件
  • 配置 Nginx 反向代理
  • 配置 SSL 证书
  • 设置 PM2 开机自启

每次部署

  • 检查 GitHub Actions 日志
  • 验证网站可访问
  • 检查 PM2 进程状态
  • 查看应用日志

定期维护

  • 更新依赖版本
  • 轮换 OAuth 凭据
  • 检查 SSL 证书有效期
  • 清理旧日志和备份

小结

Nuxt 应用的 CI/CD 部署涉及多个环节。GitHub Actions 实现自动化构建和部署流程,PM2 提供进程管理和集群模式,Nginx 处理反向代理和静态资源缓存。环境变量需要在构建时和运行时分别管理,确保敏感信息安全。遇到问题时,可以通过日志和监控工具进行排查。


参考资料: