部署与运维

部署指南

构建期用 NUXT_DEPLOY_TARGET 选择部署形态,运行期用环境变量配置数据库与存储。所有形态共用同一套代码与数据库迁移。

目标命令产物适用
cloudflare-pagespnpm build:pages → pnpm deploydist/Cloudflare Pages + D1 + R2(现有站点)
cloudflare-workerpnpm build:worker → wrangler deploy.output/Cloudflare Workers(静态资源绑定 ASSETS)
nodepnpm build:node → pnpm start.output/server/index.mjsLinux 服务器、Docker、宝塔
static-frontendpnpm build:static.output/public/前端静态托管(nginx / OSS / COS / R2),后端另行部署
vercel / netlify / aws-lambda / azure-functions / deno-deploy / firebase / bun …NUXT_DEPLOY_TARGET=vercel pnpm build平台约定目录任意 Nitro 预设,原样透传(短横线换成下划线)

怎么选:

场景推荐
海外 / 全球访问、零运维、成本最低Cloudflare Pages + D1 + R2
国内访问为主、已有服务器或宝塔node + nginx + MySQL(宝塔自带)+ OSS / COS
阿里云 FC / 腾讯云 SCF / 腾讯 EdgeOne Pages / 华为云 FunctionGraphnode 形态打成自定义运行时或容器镜像(deploy/Dockerfile),数据库用云 RDS,附件用 OSS / COS
Vercel / Netlify / AWS / Azure / Deno 等 Serverless对应 Nitro 预设 + 远程数据库(Turso / Neon / Supabase / PlanetScale / TiDB)+ 对象存储
内容站要极致首屏、后台单独部署static-frontend 放 CDN / OSS,后端任选

1. Cloudflare Pages

沿用现有流程:.cloudflare.env 提供凭据,scripts/deploy.mjs 负责构建、D1 迁移与发布。数据库固定为 D1(NUXT_DB_DRIVER 保持 sqlite),附件用 R2 绑定 BUCKET。

边缘缓存与自动清除(省 Pages Functions CPU)

每次没命中边缘缓存的页面访问都会调用一次 Pages Function(计入请求数和 CPU)。页面能在边缘缓存多久,就能省多少渲染:

  • 页面默认返回 s-maxage=600, stale-while-revalidate=600(新鲜 10 分钟,之后 10 分钟内先返回旧页面、后台刷新)。需要在站点域名的 Cloudflare 缓存规则里把页面设为「可缓存」并「遵循源站」TTL,排除 /admin、/api(/api/public/* 可单独设置)。
  • 发布新版本后,边缘缓存里的旧页面引用的是旧的 /_nuxt 文件(新部署里已不存在,404),所以 nuxtcms upgrade --deploy 和 fleet upgrade 部署成功后会按域名清除该站缓存(用 .cloudflare.env 里的 token,需要 Cache Purge 权限;预览分支不清除;--no-purge 跳过)。其他方式部署(如 pnpm run deploy)不会清除,所以默认的「先旧后新」时间保持在 10 分钟。连接了 Git 的 Pages 项目还会构建推送的升级提交,大约一分钟后用文件名不同的 chunk 替换直接上传的部署;upgrade --deploy --commit(也就是 fleet upgrade)会等这次 Git 部署完成后再清除一次缓存。
  • 自动清除:给 Pages 项目加密钥 NUXT_CF_PURGE_API_TOKEN(只需 Zone → Cache Purge,自动查找 zone 时再加 Zone → Zone → Read)后,每次公开内容变化(文章、页面、产品、菜单、设置、翻译、定时发布、商城库存……即所有调用 clearPublicCache() 的地方,含后台「清除缓存」)都会按域名清除该站的 Cloudflare 缓存;新部署的第一次页面渲染也会清除一次(不论用什么方式部署)。因此页面的边缘 TTL 自动延长到 1 天(一周内先旧后新),编辑和发版后都立即生效。按域名清除在免费套餐上可用。批量设置:nuxtcms fleet secret NUXT_CF_PURGE_API_TOKEN(隐藏输入),再 nuxtcms fleet upgrade --to <当前版本> --redeploy 让密钥生效。
    npx wrangler pages secret put NUXT_CF_PURGE_API_TOKEN --project-name <项目名>
    
    可选:NUXT_CF_PURGE_ZONE_ID(跳过 zone 查找)、NUXT_CF_PURGE_HOSTS(逗号分隔,默认是站点网址和当前请求的域名,以及各自的 www/裸域)。*.pages.dev 不在你的 zone 里,不参与清除。
  • 错误页(5xx)一律 no-store,会被缓存的 4xx 只缓存 60 秒,避免把临时故障缓存住。错误页(error.vue)同样适用:Nuxt 在一个内部请求里渲染它,并用该请求的响应头替换出错页面的响应头,所以在下一个版本之前它带的是页面 TTL(s-maxage=600,配置清缓存令牌时为一天)。页面自己的路由规则不可缓存(no-store)时,它的错误页保持该规则。
  • 静态文件的区域缓存规则(「/_nuxt/、*.js、图片:1 年」)不能缓存错误响应:边缘 TTL 和浏览器 TTL 都设为「覆盖源站」时,新部署传播期间请求到的 chunk 会以 max-age=31536000 返回 404,浏览器把这个 404 保留一年,该访客的页面一直无法水合(清缓存也没用)。Pages 本身对缺失文件返回 no-store、对 chunk 返回 immutable,所以这类规则要加边缘「状态代码 TTL」:≥ 400 不存储,浏览器 TTL 改为「遵循源站」。nuxtcms fleet cache-rules 列出所有站点域名区域的缓存规则并探测一个不存在的 chunk;--fix 修改缓存一天以上的规则(令牌需要 区域 → 缓存规则 → 编辑)。Pages 构建时会把 /** 的页面 Cache-Control 从 _headers 的兜底规则里去掉(否则 Pages 会把它拼接到每个静态文件上),chunk 只返回 public, max-age=31536000, immutable。嵌套的规则块也会这样拼接(/_nuxt/builds/latest.json 同时带 max-age=1 和 max-age=31536000),所以构建时把嵌套在另一个块里的块的 Cache-Control 移到 _headers 末尾的新块,先去掉该头(! Cache-Control)再设置:latest.json 返回 public, max-age=1, immutable,/_nuxt/builds/meta/*.json 和 chunk 返回 public, max-age=31536000, immutable。
  • 页面和公开 API 规则若把边缘 TTL 设为「覆盖源站」(如 3600 秒),会对所有状态码生效,连 private, no-store 也会被缓存:404 或部署期间的 5xx 会在边缘保留整个 TTL。这类规则也要加「状态代码 TTL」:404 缓存 60 秒(爬虫反复请求同一个不存在的地址,每个数据中心每分钟最多渲染一次;扫描不同地址时本来每个都只渲染一次),400 及以上的其他状态码「不存储」;2xx 仍用规则的 TTL。nuxtcms fleet cache-rules --short 只打印这些修改(以及长效规则的修改),不改动任何东西;--fix --short 执行修改。1 年的静态规则还会排除 /_nuxt/builds/latest.json(Nuxt 轮询它来发现新部署):缓存一年时,只有部署清缓存后才会更新。排除后它不在边缘缓存(.json 不是默认缓存的扩展名),作为静态文件也不消耗 Functions CPU;--fix 会按 URL 清掉边缘已缓存的那份。
  • 这种覆盖也会缓存已登录请求的响应:后台页面在 SSR 时渲染管理员的数据(/zh/admin/users、/zh/admin/contacts),草稿预览在公开地址和公开 API 上渲染草稿,站点也可能渲染账户页。被缓存一次后,同一数据中心下一个访问该地址的人就会拿到它。只排除以 /admin 开头路径的规则漏掉了 /<语言>/admin。nuxtcms fleet cache-rules --short 会给这类规则加上 and not (...):带会话 cookie(auth_token、cms_preview、shop_member、saas_user、hub_session)的请求,以及以 /admin、/setup 结尾或包含 /admin/ 的路径,交给 Cloudflare 默认行为(不缓存)。匿名访客照常缓存。
  • /_i18n/<哈希>/<语言>/messages.json 缓存一年(网址随语言文件内容变化)。
  • 站点自己在 routeRules 里给页面设置的缓存头保持不变,不会被延长。

冷启动更省:CF_PAGES_NO_BUNDLE=1

访问量小的站,大部分渲染都落在新的 isolate 上,而新 isolate 要先编译 worker。默认情况下 wrangler pages deploy 会把 Nitro 已分好块的 dist/_worker.js(含后台页面)重新打成一个约 6 MB 的大模块, 每个 isolate 都要编译一遍。在 .cloudflare.env 里设 CF_PAGES_NO_BUNDLE=1 后,nuxtcms upgrade --deploy、 fleet upgrade 和 pnpm run deploy 会加上 --no-bundle:按构建结果原样上传,isolate 只编译请求用到的块 (zbservice:isolate 启动时的编译约 120 ms -> 约 50 ms;本地 workerd 上请求本身的 CPU 没有变化)。wrangler 会提示 nodejs_compat_v2 的 polyfill 不再注入,Nitro 的产物不需要它。wrangler pages dev --no-bundle 跑不起这种产物(本地试用请用 wrangler dev 加 no_bundle)。

2. Cloudflare Workers

  1. 复制 node_modules/nuxtcms/deploy/wrangler.worker.toml.example 为 wrangler.toml,填入 D1 / R2 ID。
  2. pnpm build:worker
  3. wrangler d1 migrations apply <db> --remote
  4. wrangler deploy

Workers 与 Pages 使用相同的 SQLite 迁移文件。若要在 Workers 上使用 PostgreSQL / MySQL,创建 Hyperdrive 并绑定为 HYPERDRIVE,构建时设置 NUXT_DB_DRIVER=pg|mysql。

3. Linux 服务器(含宝塔)

pnpm install
NUXT_DB_DRIVER=mysql pnpm build:node          # 或 sqlite / pg
DATABASE_URL=mysql://user:[email protected]:3306/nuxtcms NUXT_DB_AUTO_MIGRATE=1 pnpm start
  • 进程守护:pm2 start deploy/ecosystem.config.cjs --env production。宝塔「网站 → Node 项目」本质是 PM2,把启动文件填 .output/server/index.mjs,环境变量填在项目设置里即可;宝塔自带的 MySQL 直接作为 DATABASE_URL。
  • 反向代理:node_modules/nuxtcms/deploy/nginx-node-proxy.conf.example,/_nuxt/ 由 nginx 直接吐静态文件。
  • Docker:node_modules/nuxtcms/deploy/Dockerfile,--build-arg NUXT_DB_DRIVER=pg 选择驱动。
  • 附件:本地磁盘(默认 .data/uploads)或 NUXT_STORAGE_DRIVER=oss|cos|s3|r2,详见 README「附件存储」。

4. 前后端分离

前端构建为纯静态 SPA(pnpm build:static,产物在 .output/public),后端只负责接口和动态生成的文件。

推荐:同域部署。 nginx 托管静态文件,并把 /api/、robots.txt、sitemap.xml、/__sitemap__/、llms.txt、llms-full.txt 和 IndexNow 密钥文件反代到后端。前端与接口同源,不需要 CORS,也没有跨域 Cookie 问题,NUXT_PUBLIC_API_BASE 留空。配置模板:node_modules/nuxtcms/deploy/nginx-static-frontend.conf.example(已在 nginx 1.28 上用真实站点验证)。

后端(Pages / Workers / node 任选)运行时增加:

NUXT_TRUSTED_PROXIES=<nginx 服务器的公网 IP>   # 限流和人机验证按真实访客 IP 计算,而不是 nginx 的 IP

模板要点:

  • SPA 兜底用 200.html,不要用 index.html:根目录的 index.html 只是跳到默认语言的页面。构建时会自动把 200.html / 404.html 换成真正的 SPA 外壳;之前版本构建的产物可以手动执行 node node_modules/nuxtcms/scripts/spa-fallback.mjs .output/public。
  • 构建时预渲染出的 robots.txt、sitemap.xml 不是实时内容,nginx 一律交给后端。
  • client_max_body_size 20m:nginx 默认只允许 1 MB,上传图片会被 413 拒绝。
  • HTML 外壳 no-cache(发新版后立即生效),/_nuxt/ 缓存一年。

跨域部署(前端 www.example.com,后端 api.example.com,不经过 nginx 反代):

# 后端运行时
NUXT_CORS_ORIGINS=https://www.example.com      # 允许的前端来源,逗号分隔
NUXT_AUTH_COOKIE_SAME_SITE=none                # 跨域携带登录 Cookie,必须 HTTPS
NUXT_STORAGE_PUBLIC_URL=https://cdn.example.com # 媒体走 CDN,避免相对路径指向前端域
# 前端构建
NUXT_PUBLIC_API_BASE=https://api.example.com pnpm build:static

前后端请用同一主域名下的子域名。完全不同的域名之间(如 example.cn 与 xxx.pages.dev),登录 Cookie 会被 Safari 等浏览器当作第三方 Cookie 拦截,后台无法登录。robots.txt、sitemap 等此时由后端域名提供。

限制:

  • SPA 模式没有服务端渲染,标题、描述、hreflang 要等浏览器执行 JS 后才出现,搜索引擎和 AI 爬虫看到的基本是空页面。需要 SEO / AI 搜索的公开站点建议用前三种形态,或第 6 节的香港反代。
  • 后端的跳转规则(站点 routeRules 里的 redirect、后台「跳转」里的域名与路径规则)不作用于静态页面;带 ** 的规则在浏览器端不会展开。
  • 按来源域名放行的第三方服务(在线客服、统计等)要加上新的前端域名。
  • /docs(Nuxt Content)在 SPA 模式下走客户端查询。
  • 所有数据都由浏览器自己请求,所以被多个组件同时调用的共享 useAsyncData(站点设置、菜单)要加 dedupe: 'defer':Nuxt 默认的 'cancel' 会让每个调用方重新发起一次请求(SSR 下有 payload,看不出来)。框架自带的 composable 已经这样做;站点里自己复制的(useSite、站点自己的设置 composable)请检查一下,同一个 URL 的数据用同一个 key。

5. Serverless 与其他云平台

Serverless 平台没有持久磁盘,所以:

  • 数据库必须是远程的:
    • SQLite 方言:Turso(libSQL),DATABASE_URL=libsql://xxx.turso.io + DATABASE_AUTH_TOKEN=...,迁移与 D1 共用;
    • PostgreSQL:Neon、Supabase、阿里云 / 腾讯云 RDS,构建时 NUXT_DB_DRIVER=pg;
    • MySQL:PlanetScale、TiDB Cloud、云 RDS,构建时 NUXT_DB_DRIVER=mysql。
  • 附件用对象存储:NUXT_STORAGE_DRIVER=s3|oss|cos|r2。
  • 接口缓存自动改用内存(不再写本地文件),构建期预渲染自动关闭。
  • 定时任务:用平台自带的 Cron(Vercel Cron、Netlify Scheduled Functions、EventBridge…)或 deploy/cron-worker 定时请求 POST /api/cron/dispatch。

示例(Vercel):

NUXT_DEPLOY_TARGET=vercel NUXT_DB_DRIVER=pg pnpm build
# 在 Vercel 项目里配置 DATABASE_URL、NUXT_JWT_SECRET、NUXT_STORAGE_* 等环境变量

阿里云 FC / 腾讯云 SCF:使用 deploy/Dockerfile 构建镜像,以「自定义容器」方式部署,监听端口 3000;数据库与对象存储走内网地址可省流量费。

6. 中国大陆访问(香港反向代理)

部署在 Cloudflare 上的站点,大陆访客走的是 Cloudflare 的海外线路,晚高峰常常很慢甚至打不开。没有 ICP 备案时,可以在香港(或新加坡、日本)放一台 nginx 服务器做反向代理:大陆访客 → 香港服务器 → Cloudflare Pages。站点本身不用改动。

  1. 服务器:选香港区域、带 CN2 GIA / 三网优化线路的云服务器(1 核 1G 就够),装好 nginx。
  2. nginx 配置:复制 deploy/nginx-hk-proxy.conf.example,把 www.example.cn 换成你的域名,把 your-project.pages.dev 换成 Pages 项目地址。构建产物 /_nuxt/ 会缓存在香港服务器上,页面和接口每次都回源。
  3. DNS,二选一:
    • 全部解析到香港服务器:最简单,海外访客也会多绕一跳;
    • 分线路解析(DNSPod、阿里云解析的「境内 / 境外」线路):境内线路指向香港服务器,境外线路继续 CNAME 到 Cloudflare。这种方式需要把域名的 NS 放在 DNSPod 或阿里云。
  4. 证书:香港服务器需要自己的 HTTPS 证书(certbot、acme.sh)。如果用了分线路解析,要用 DNS 验证方式申请:Let's Encrypt 从海外发起验证,会落到 Cloudflare 而不是这台服务器。
  5. 真实访客 IP:在 Pages 的环境变量里设置 NUXT_TRUSTED_PROXIES=<香港服务器公网 IP>(多个用逗号分隔,也可以写 IPv4 网段),然后重新部署。不设置的话,所有大陆访客在登录限流、验证码和支付请求里都会被当成香港服务器这一个 IP,一个人输错密码就可能把所有人挡在外面。只有来自这些地址的请求才会采用它们转发的 X-Forwarded-For,其他人伪造这个请求头没有作用。

检查:在大陆网络下打开站点,响应头里 /_nuxt/ 文件有 X-Proxy-Cache: HIT,后台登录、上传和下单都能正常完成。

环境变量速查

变量阶段说明
NUXT_DEPLOY_TARGET构建部署形态
NUXT_DB_DRIVER构建sqlite / pg / mysql
DATABASE_URL运行连接串(Cloudflare 上忽略,用绑定)
DATABASE_AUTH_TOKEN运行远程 libSQL(Turso)令牌
NUXT_DB_AUTO_MIGRATE运行1 = 启动时执行迁移(node 形态)
NUXT_STORAGE_*运行附件存储驱动与凭据
NUXT_PUBLIC_API_BASE构建静态前端指向的后端地址
NUXT_CORS_ORIGINS运行后端允许的前端来源
NUXT_AUTH_COOKIE_SAME_SITE运行跨域时设为 none
NUXT_TRUSTED_PROXIES运行站点前面的反向代理 IP(如香港 nginx),用它转发的访客 IP
NUXT_CF_PURGE_API_TOKEN运行公开内容变化时按域名清除 Cloudflare 缓存,并把页面边缘 TTL 延长到 1 天(Pages 密钥)
NUXT_CF_PURGE_ZONE_ID / NUXT_CF_PURGE_HOSTS运行可选:zone 与要清除的域名
NUXT_PAGE_CACHE_PURGED_MAX_AGE / NUXT_PAGE_CACHE_PURGED_SWR运行有清除 token 时页面的边缘 TTL(默认 86400 / 604800 秒)
NUXT_DEMO_MODE构建true = 公开演示站点(见下文),真实站点不要开启

演示站点模式

构建时设置 NUXT_DEMO_MODE=true,站点就变成公开演示站(例如官网的商城演示):

  • 每个页面底部显示演示提示条和「进入后台演示」按钮,登录页多一个「一键进入演示后台」按钮,都通过 /api/auth/demo 以共享的演示管理员身份登录(无需密码)。
  • 可能泄露密钥、执行代码或被滥用的后台操作一律禁止:用户与密码、邮件配置、安全验证、站点设置、插件(安装、设置、插件中心)、备份、上传、定时任务、翻译与语言。联系留言、订阅者和导出不可查看。后台保存的 HTML 会去掉脚本、iframe、表单和事件属性。
  • 每天自动恢复一次数据(站点时区零点后的第一个请求,时区取 NUXT_CRON_TZ_OFFSET):清空内容、用户、联系留言并写回演示内容。包插件通过 Nitro 钩子 nuxtcms:demo-reset 恢复自己的数据(商城会写回演示价格和优惠码,并删除订单)。空数据库在第一个请求时自动填充。
  • 商城:币种为人民币时无需密钥即显示微信支付和支付宝,付款为模拟:订单页显示二维码(手机扫码会打开订单页)和「模拟支付成功」按钮,结账页预填示例信息。

不要在真实站点开启,它每天都会删除数据。