Nuxt 项目部署到宝塔:PM2、Nginx 与 HTTPS

从构建完整产物到配置 PM2、Nginx 和 HTTPS,介绍 Nuxt SSR 项目部署到宝塔的完整流程,并提供更新发布、版本回退和常见故障排查方法。文中使用通用域名与路径示例。

本文使用虚构的项目名、域名和通用路径,介绍 Nuxt SSR 项目通过宝塔、PM2 和 Nginx 部署的完整流程。示例按 npm 包管理器和 Nitro Node 服务构建方式编写。

1. 示例配置与占位符

项目 示例配置
本地源码目录 /path/to/nuxt-app
服务器部署目录 /www/wwwroot/nuxt-app
宝塔项目名称 / PM2 进程名 nuxt-app
网站地址 https://app.example.com
生产接口地址 https://api.example.com
Nuxt 监听地址 127.0.0.1:3000
运行用户 www
Node 版本占位符 NODE_VERSION,替换成面板安装的兼容版本目录名
Node 可执行文件 /www/server/nodejs/NODE_VERSION/bin/node

操作前,把示例域名、源码路径、部署目录和 NODE_VERSION 替换成自己的配置。example.com 是示例域名,不能用于申请自己的证书。NODE_VERSION 不是命令自动识别的变量,需要手动替换成真实版本目录名。

请求经过的服务:

text 复制代码
访客 → 网站域名的 Nginx(80 / 443)→ Nuxt(127.0.0.1:3000)
                                          ↓
                     后端接口(api.example.com)

SSR 渲染时,Nuxt 服务端也会请求后端;浏览器中的交互也可能直接请求后端。这里使用两端都能访问的完整 HTTPS 接口地址。

2. 准备服务器与目录

在宝塔中准备好:

  1. Nginx。
  2. 项目兼容的 Node.js 版本,可通过「网站 → Node 项目 → Node 版本管理器」安装。
  3. Node 项目中的 PM2 管理功能。支持的面板版本会提供「PM2 项目」入口,具体位置以面板界面为准。
  4. 将网站域名解析到服务器,确保云安全组及服务器防火墙允许网站使用的 80、443 端口。

需要执行本文的 pm2 命令时,在 Node 项目页把「命令行版本」设为对应的 Node 版本,再打开新的宝塔终端,确认 node -v 和 pm2 -v 能正常输出。

Nuxt 监听本机 127.0.0.1:3000,由 Nginx 转发访问,因此无需对公网放行 3000 端口。

先通过宝塔文件管理器或服务器终端创建独立目录:

bash 复制代码
mkdir -p /www/wwwroot/nuxt-app

部署完成后的主要结构:

text 复制代码
/www/wwwroot/nuxt-app/
├── .output/
│   ├── public/                      # 前端静态资源
│   ├── server/                      # 服务端代码与构建时收集的依赖
│   │   └── index.mjs                # Nuxt 实际入口
│   └── nitro.json
├── ecosystem.config.cjs             # PM2 运行配置
├── .well-known/
│   └── acme-challenge/              # SSL 文件验证目录
└── deploy-backups/                  # 发布与配置备份

压缩包、__MACOSX 等归档附属文件也可以放在 nuxt-app 内。运行所需的是完整的 .output 和 PM2 配置;已有归档文件可以按自己的备份习惯保留。

3. 确认 Nuxt 的接口配置

在 nuxt.config.ts 中声明可覆盖的接口地址,例如:

ts 复制代码
export default defineNuxtConfig({
  ssr: true,
  runtimeConfig: {
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:8080',
    },
  },
})

请求封装通过 useRuntimeConfig().public.apiBase 读取接口地址。生产环境示例:

dotenv 复制代码
NUXT_PUBLIC_API_BASE=https://api.example.com

例如文章列表的最终地址是:

text 复制代码
https://api.example.com/articles

环境变量名 NUXT_PUBLIC_API_BASE 对应 runtimeConfig.public.apiBase。其他配置键需要使用对应的 Nuxt 环境变量名。

开发和构建时,Nuxt 会读取源码根目录中的 .env。直接启动构建后的服务器时,Nuxt 不会自动读取 .env,生产变量应由启动进程传入。本文统一放在下一节的 PM2 配置中。Nuxt 环境变量说明

本教程采用完整接口地址。若改为 /api 等相对地址,还需要处理 Nuxt 服务端的代理路由;只给浏览器所在域名配置 Nginx 代理,并不能保证 SSR 内部请求也能访问该路径。

4. 构建并上传完整产物

4.1 本地构建

在电脑终端执行:

bash 复制代码
cd /path/to/nuxt-app
npm ci
npm run build

示例假定项目已有 package-lock.json,因此使用 npm ci 安装锁定版本的依赖。使用 pnpm 或 yarn 的项目应采用对应的安装、构建命令和锁文件。日常依赖未变化时,可以直接重新构建。

确认构建结果包含:

text 复制代码
.output/public/
.output/server/index.mjs
.output/server/node_modules/

Nuxt 的 Node 服务入口是 .output/server/index.mjs。服务器运行时需要完整 .output;静态资源、服务端代码和收集到的依赖应一起上传。Nuxt Node 部署说明

如果构建环境自动选择了其他托管平台的 preset,可以明确使用:

bash 复制代码
NITRO_PRESET=node-server npm run build

4.2 打包

在源码根目录执行,macOS 可以使用:

bash 复制代码
COPYFILE_DISABLE=1 tar --exclude='.DS_Store' -czf nuxt-app-output.tar.gz .output

这样打包会包含隐藏的 .output 目录,并减少 macOS 附属文件。也可以直接上传完整 .output 文件夹,或使用已有的 .output.zip。

4.3 上传和解压

通过宝塔「文件」进入:

text 复制代码
/www/wwwroot/nuxt-app

上传压缩包,并在这个目录内解压。使用上述 tar 包时,服务器终端执行:

bash 复制代码
cd /www/wwwroot/nuxt-app
tar -xzf nuxt-app-output.tar.gz
ls -l .output/server/index.mjs

正确入口是:

text 复制代码
/www/wwwroot/nuxt-app/.output/server/index.mjs

确认没有多套一层同名文件夹。宝塔文件管理器需要显示隐藏文件,才能看到 .output。

本地构建后可以上传完整产物。若项目包含原生二进制依赖,部署时出现平台不兼容错误,应在与服务器兼容的 Linux 环境重新安装依赖并构建。

5. 创建 PM2 配置

在服务器的 /www/wwwroot/nuxt-app 下新建 ecosystem.config.cjs:

js 复制代码
module.exports = {
  apps: [
    {
      name: 'nuxt-app',
      cwd: '/www/wwwroot/nuxt-app',
      script: '/www/wwwroot/nuxt-app/.output/server/index.mjs',
      interpreter: '/www/server/nodejs/NODE_VERSION/bin/node',
      uid: 'www',
      gid: 'www',
      exec_mode: 'fork',
      instances: 1,
      watch: false,
      autorestart: true,
      max_memory_restart: '512M',
      out_file: '/www/wwwlogs/nodejs/nuxt-app/out.log',
      error_file: '/www/wwwlogs/nodejs/nuxt-app/err.log',
      log_date_format: 'YYYY-MM-DD HH:mm:ss',
      env: {
        NODE_ENV: 'production',
        NITRO_HOST: '127.0.0.1',
        NITRO_PORT: '3000',
        NUXT_PUBLIC_API_BASE: 'https://api.example.com',
      },
    },
  ],
}

服务器终端创建日志目录,并检查配置语法:

bash 复制代码
mkdir -p /www/wwwlogs/nodejs/nuxt-app
/www/server/nodejs/NODE_VERSION/bin/node --check /www/wwwroot/nuxt-app/ecosystem.config.cjs

主要字段含义:

字段 用途
name PM2 进程名,保持与宝塔项目名一致
cwd 进程工作目录
script Nuxt 真正的启动入口
interpreter 明确使用哪个 Node 可执行文件
uid / gid 使用 www 用户与用户组运行应用
instances: 1 使用一个实例,可根据服务器容量调整
watch: false 上传文件时不自动触发反复重启
max_memory_restart 超过设置的内存阈值后由 PM2 重启应用,并非操作系统硬性内存限制
env 生产运行环境变量

使用 .cjs 扩展名,可以明确采用 CommonJS 的 module.exports 写法。本教程后续以这个文件作为运行参数的统一来源。修改其 env 后,应重启应用让参数生效。PM2 配置说明

6. 在宝塔创建 PM2 项目

入口:网站 → Node 项目 → 添加项目 → PM2 项目。

表单字段 填写内容
项目名称 nuxt-app
Node 版本 选择与 interpreter 对应的已安装版本
启动文件 /www/wwwroot/nuxt-app/ecosystem.config.cjs
运行目录 /www/wwwroot/nuxt-app
负载实例数量 1
内存上限 512 MB,根据实际容量调整
自动重载 关闭
不安装 node_module 勾选,使用完整构建产物

点击表单内的 「更多配置」,继续填写:

字段 填写内容
参数 留空
环境变量 已在 ecosystem.config.cjs 中集中设置,可留空;已有同名配置时保持一致
运行用户 www
项目端口 3000
放行端口 不勾选
绑定域名 app.example.com,不带协议和路径

宝塔表单的「启动文件」填写 PM2 配置文件;配置文件中的 script 再指向 Nuxt 的 index.mjs。

不同宝塔版本处理启动文件的方式可能不同。如果直接填写 index.mjs 后,出现进程被命名为 index、表单变量未生效或状态识别不一致,应检查实际启动命令。显式的 ecosystem 配置便于统一进程名和运行参数,最终以运行中的进程信息为准。

保存创建后确认:

  • 项目显示「运行中」,有 PID。
  • 根目录显示 /www/wwwroot/nuxt-app。
  • 项目日志里没有持续报错或重复重启。

在宝塔终端中也可以核对:

bash 复制代码
pm2 list
pm2 describe nuxt-app
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/articles

本机页面应返回 200。Nuxt 的端口、监听地址可以由 NITRO_PORT 和 NITRO_HOST 设置。Nuxt 服务运行参数

确认进程正确后保存 PM2 列表:

bash 复制代码
pm2 save

pm2 save 保存的是进程列表。服务器重启后的自动恢复,还需要宝塔或 PM2 的开机启动机制支持,应单独确认。

7. 检查 Nginx 外网映射

进入项目设置,检查域名管理和外网映射。网站域名应代理到:

text 复制代码
http://127.0.0.1:3000

最终反向代理应具有下面这样的效果:

nginx 复制代码
location / {
    proxy_pass http://127.0.0.1:3000;
    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;
}

宝塔通常会生成代理配置,可能放在主配置引用的文件里。若已经存在 location /,检查或修改已有规则,避免重复添加相同的 location。

页面路由由 Nuxt 接收,例如 /articles、/search;实际测试时使用项目已有的路由。保持请求路径完整地转发到 Nuxt。

SSL 文件验证目录

示例项目的宝塔 Node 站点主配置文件通常类似:

text 复制代码
/www/server/panel/vhost/nginx/node_nuxt-app.conf

在对应的 server { ... } 内补充:

nginx 复制代码
location ^~ /.well-known/acme-challenge/ {
    root /www/wwwroot/nuxt-app;
    default_type text/plain;
    try_files $uri =404;
}

同时确保目录存在:

bash 复制代码
mkdir -p /www/wwwroot/nuxt-app/.well-known/acme-challenge

映射关系为:

text 复制代码
请求:/.well-known/acme-challenge/某个验证文件
文件:/www/wwwroot/nuxt-app/.well-known/acme-challenge/某个验证文件

这里的 ^~ 使这条匹配到的最长前缀规则跳过同层正则 location 的检查,验证请求会由这个目录处理。Nginx location 说明

修改前备份站点配置,并保留宝塔原有的 SSL 管理标记、证书验证 include 和其他设置。以上是局部片段,不应直接覆盖整个站点配置。

先检查语法:

bash 复制代码
/www/server/nginx/sbin/nginx -t

只有显示检查通过后,再执行重载:

bash 复制代码
/www/server/nginx/sbin/nginx -s reload

8. 申请 SSL 并开启 HTTPS

  1. 确认宝塔项目处于「运行中」。
  2. 确认 http://app.example.com 能访问,域名解析正确。
  3. 打开项目设置 → SSL → 免费证书 → 申请证书。
  4. 品牌选择 Let's Encrypt,验证方式选择 文件验证。
  5. 勾选 app.example.com,申请并完成部署;如果签发后没有自动部署,再点击该证书的「部署」。
  6. 打开 https://app.example.com 确认证书有效,再开启「强制 HTTPS」。

常见的 SSL 配置问题有两类:

  • 提示先启动项目:先修复宝塔识别不到进程的问题,再进入 SSL 设置。
  • 验证 URL 返回 404:检查验证文件实际写入目录,确保与上面的 Nginx root 一致。

需要验证路径时,可以在该目录手动创建一个临时文本文件,例如 nuxt-acme-path-check.txt,写入 nuxt-acme-ok,然后访问:

text 复制代码
http://app.example.com/.well-known/acme-challenge/nuxt-acme-path-check.txt

返回相同文本后,删除自己创建的临时测试文件。这个检查只用于核对目录映射,不代替证书机构生成的实际验证文件。

项目目录迁移后,宝塔项目运行目录与 Nginx 验证目录也要同步更新,供后续文件验证和续签使用。

9. 部署后验证

9.1 网页、HTTPS 与静态资源

在浏览器中检查:

  • 访问 HTTP 地址后能跳转到 HTTPS。
  • 刷新 /articles、/search?q=example 等地址正常。
  • 文章列表、分类、搜索结果正常。
  • 开发者工具 Network 中的 /_nuxt/ JavaScript 和 CSS 能正常加载。

服务器或电脑终端可以执行:

bash 复制代码
curl -sS -L -o /dev/null -w '%{url_effective} %{http_code}\n' http://app.example.com/articles

应最终到达 HTTPS 页面并返回 200。

9.2 后端接口

bash 复制代码
curl -sS 'https://api.example.com/articles?page=1&pageSize=10'

将示例路径 /articles 替换成后端实际提供的公开接口,并按后端约定检查 JSON 内容。例如,若后端以 code: 200 表示成功,应同时检查这个业务字段。HTTP 状态为 200 本身不能证明业务请求成功。

跨域响应头可以这样检查:

bash 复制代码
curl -sS -D - -o /dev/null \
  -H 'Origin: https://app.example.com' \
  'https://api.example.com/articles?page=1&pageSize=10'

后端应按业务需要允许示例前端 Origin,并返回相应的跨域响应头。浏览器还需实际完成一次搜索等交互,确认客户端请求正常。

诊断公开后端接口时,优先使用真实的 GET 请求。curl -I 发送的是 HEAD,而后端可能对 HEAD 和 GET 使用不同的鉴权规则。

9.3 进程与日志

bash 复制代码
pm2 describe nuxt-app
tail -n 80 /www/wwwlogs/nodejs/nuxt-app/err.log

确认进程入口和工作目录都在 /www/wwwroot/nuxt-app 下,进程持续运行,日志没有新增的启动错误。

10. 后续更新与回退

更新发布

  1. 本地重新执行 npm run build,打包新的完整 .output。
  2. 把新的 nuxt-app-output.tar.gz 上传到 /www/wwwroot/nuxt-app。
  3. 在宝塔中停止 nuxt-app 项目,准备切换产物。
  4. 在服务器终端运行下面的脚本。脚本先解压检查新入口,再备份旧产物并切换。
bash 复制代码
bash <<'BASH'
set -euo pipefail
cd /www/wwwroot/nuxt-app
test -f nuxt-app-output.tar.gz
nuxt_release_dir=$(mktemp -d /www/wwwroot/nuxt-app/.release.XXXXXX)
tar -xzf nuxt-app-output.tar.gz -C "$nuxt_release_dir"
test -f "$nuxt_release_dir/.output/server/index.mjs"
test -d "$nuxt_release_dir/.output/public"
nuxt_deploy_stamp=$(date +%Y%m%d-%H%M%S)
mkdir -p deploy-backups
mv .output "deploy-backups/output-$nuxt_deploy_stamp"
mv "$nuxt_release_dir/.output" .output
printf '旧产物备份:%s\n' "/www/wwwroot/nuxt-app/deploy-backups/output-$nuxt_deploy_stamp"
printf '临时解压目录:%s\n' "$nuxt_release_dir"
BASH
  1. 确认脚本成功后,从宝塔启动项目;检查网页、接口、日志,再执行 pm2 save。
  2. 临时解压目录和归档文件可以在确认发布成功后清理,上一版产物按自己的备份周期保留。

更新时保留 ecosystem.config.cjs、.well-known 和备份目录。只有运行变量变化时才修改 ecosystem 文件;更改域名、目录、监听端口或证书配置时,再检查对应的 Nginx 设置。

如果是只修改 PM2 环境变量,可以在配置文件保存后,通过宝塔重启项目;也可以在同一套 PM2 管理环境中运行:

bash 复制代码
pm2 restart /www/wwwroot/nuxt-app/ecosystem.config.cjs --update-env
pm2 save

更新失败时回退

  1. 在宝塔中停止项目。
  2. 将当前有问题的 .output 移到 deploy-backups 下,使用一个新的名字保存。
  3. 找到刚才打印的 deploy-backups/output-时间戳,把它移回 .output。
  4. 如果同时改过运行配置,恢复对应的配置备份。
  5. 启动项目,重新检查页面、接口与日志。

如果脚本在解压或入口检查阶段失败,旧 .output 尚未切换;检查错误后可以直接启动原有项目。

11. 常见故障对照

现象 优先检查 处理方向
HTTP 能访问,宝塔显示未启动 pm2 list 中的名字与宝塔项目名是否一致 使用显式 ecosystem 文件,统一为 nuxt-app,再通过宝塔保存配置并重启
提示启动成功,但仍显示未启动 是否只是启动命令执行成功;日志里实际进程叫什么 结合 PM2 状态、PID、端口和错误日志判断,不只看提示框
SSL 提示先启动项目 宝塔是否已正确识别进程 先修复 PM2 名称与状态,再配置 SSL
SSL 文件验证 404 实际验证文件路径与 Nginx root 是否一致 检查 .well-known/acme-challenge 映射,修正后测试并重载 Nginx
Nginx 返回 502 Nuxt 是否运行、是否监听 3000、代理目标是否一致 检查本机 curl http://127.0.0.1:3000 和项目错误日志
日志出现 EADDRINUSE 3000 端口是否被另一个进程占用 使用 ss -lntp 查清占用进程归属,再处理重复启动
页面有内容,浏览器交互请求失败 实际接口地址、HTTPS 证书、跨域响应 检查完整请求 URL 和返回内容;核对 ecosystem 的 env 是否生效
接口请求指向 localhost:8080 运行环境仍使用源码中的默认值 传入 NUXT_PUBLIC_API_BASE 并重启;公共配置会被浏览器使用
/_nuxt/ 资源 404 是否上传了完整 .output/public,新进程是否已切换到新目录 补全构建产物,核对启动路径、工作目录和文件权限
修改 .env 后线上没变化 是否直接运行构建后的服务器 修改 ecosystem 的 env,再重启进程
移动目录后网站或续签异常 宝塔运行目录、PM2 cwd/script、Nginx 验证目录 同步更新这几处,验证成功后保存 PM2 列表

部署完成后,应确认独立的项目目录、PM2 运行参数、Nginx 代理目标和证书验证目录相互一致。