Deployment
NUXT_DEPLOY_TARGET selects the deployment shape at build time; environment variables configure database and storage at runtime. Every target shares the same code and migrations.
| Target | Commands | Output | Use case |
|---|---|---|---|
cloudflare-pages | pnpm build:pages → pnpm deploy | dist/ | Cloudflare Pages + D1 + R2 (existing sites) |
cloudflare-worker | pnpm build:worker → wrangler deploy | .output/ | Cloudflare Workers with static assets (ASSETS binding) |
node | pnpm build:node → pnpm start | .output/server/index.mjs | Linux servers, Docker, Baota / aaPanel |
static-frontend | pnpm build:static | .output/public/ | frontend on nginx / OSS / COS / R2 static hosting, backend deployed separately |
vercel / netlify / aws-lambda / azure-functions / deno-deploy / firebase / bun … | NUXT_DEPLOY_TARGET=vercel pnpm build | platform default | any Nitro preset, passed through (dashes become underscores) |
Choosing:
| Situation | Recommended |
|---|---|
| global audience, zero ops, lowest cost | Cloudflare Pages + D1 + R2 |
| mainland-China audience, own server or Baota | node + nginx + MySQL + OSS / COS |
| Alibaba Cloud FC / Tencent SCF / EdgeOne Pages / Huawei FunctionGraph | the node build as a custom runtime or container (deploy/Dockerfile), cloud RDS, OSS / COS |
| Vercel / Netlify / AWS / Azure / Deno serverless | the matching Nitro preset + a remote database (Turso / Neon / Supabase / PlanetScale / TiDB) + object storage |
| fastest first paint for a content site, admin elsewhere | static-frontend on a CDN / OSS, backend anywhere |
1. Cloudflare Pages
Unchanged flow: .cloudflare.env provides credentials and scripts/deploy.mjs builds, applies D1 migrations and publishes. The database is D1 (keep NUXT_DB_DRIVER=sqlite) and media uses the R2 binding BUCKET.
Edge cache and automatic purge (saves Pages Functions CPU)
Every page view that misses the edge cache calls the Pages Function (requests and CPU). The longer pages stay at the edge, the fewer renders:
- Pages are sent with
s-maxage=600, stale-while-revalidate=600(fresh for 10 minutes, then served stale for 10 more while refreshing). The site's Cloudflare cache rule must make pages eligible for cache and respect the origin TTL, excluding/adminand/api(/api/public/*may get its own rule). - After a release, a page still cached from the previous deployment points at
/_nuxtfiles the new deployment no longer has (404).nuxtcms upgrade --deployandfleet upgradetherefore purge the site's host names after a successful deploy (with the.cloudflare.envtoken, which needs Cache Purge; not for preview branches;--no-purgeskips it). Other deploys (e.g.pnpm run deploy) do not, which is why the default stale window stays at 10 minutes. A Pages project connected to Git also builds the pushed upgrade commit and replaces the direct upload about a minute later, with other chunk names;upgrade --deploy --commit(and sofleet upgrade) waits for that Git deployment and purges again. - Automatic purge: with the Pages secret
NUXT_CF_PURGE_API_TOKEN(Zone → Cache Purge, plus Zone → Zone → Read for the zone lookup), every public change (posts, pages, products, menus, settings, translations, scheduled publishing, shop stock... everything that callsclearPublicCache(), including the admin's "clear cache") purges the site's host names from the Cloudflare cache, and the first page render of a new deployment purges once (however it was deployed). The page edge TTL then becomes a day (a week stale-while-revalidate), and edits and releases still show at once. Purge by host name works on every plan. For the whole fleet:nuxtcms fleet secret NUXT_CF_PURGE_API_TOKEN(hidden input), thennuxtcms fleet upgrade --to <current version> --redeployso the secret reaches a deployment.Optional:npx wrangler pages secret put NUXT_CF_PURGE_API_TOKEN --project-name <project>NUXT_CF_PURGE_ZONE_ID(skips the zone lookup),NUXT_CF_PURGE_HOSTS(comma list; default: the site URL host and the request host, each with its www / apex twin).*.pages.devis not in your zone and is not purged. - Server errors (5xx) are
no-store; cacheable 4xx are kept 60 seconds, so a transient failure is not cached. This includes the error page (error.vue): Nuxt renders it in an internal request whose headers replace those of the failed page, and before the next release it sent the page TTL (s-maxage=600, with a purge token a day). A page whose own route rule is not cacheable (no-store) keeps that rule on its error page. - A zone cache rule for static files ("
/_nuxt/,*.js, images: 1 year") must not keep errors: with edge and browser TTL on "override origin", a chunk asked for while a new deployment propagates answers 404 withmax-age=31536000, the browser keeps that 404 for a year and the page never hydrates for that visitor (a purge does not help). Pages itself sendsno-storefor missing files andimmutablefor chunks, so such a rule needs edge Status code TTL "≥ 400: no store" and browser TTL "respect origin".nuxtcms fleet cache-ruleslists the rules of every site's zones and probes a missing chunk;--fixchanges the rules that keep responses a day or more (token: Zone → Cache Rules → Edit). Pages builds leave the page Cache-Control of/**out of the_headerscatch-all (Pages would join it onto every static file), so chunks send onlypublic, max-age=31536000, immutable. Nested blocks are joined the same way (/_nuxt/builds/latest.jsongotmax-age=1andmax-age=31536000), so the Cache-Control of a block inside another one moves to a block at the end of_headersthat detaches the header first (! Cache-Control):latest.jsonsendspublic, max-age=1, immutable,/_nuxt/builds/meta/*.jsonand chunkspublic, max-age=31536000, immutable. - Page and public API rules with an edge TTL of "override origin" (for example 3600 s) apply it to every status and even to
private, no-store: a 404 or a 5xx during a deployment stays at the edge for the whole TTL. Give them a Status code TTL too:40460 seconds (a bot asking for one missing URL again and again costs at most one render a minute per data center; scans of distinct URLs render each one once anyway), every other status from 400 up "no store"; 2xx keep the rule's TTL.nuxtcms fleet cache-rules --shortprints these changes (and those of long rules) without changing anything;--fix --shortapplies them. The 1-year static rule also leaves out/_nuxt/builds/latest.json(the build id Nuxt polls to notice a new deployment): kept a year, it only changed when a deploy purged the zone. Outside the rule it is not edge cached (.jsonis not a default cached extension) and, as a static file, costs no Functions CPU;--fixpurges the copy the edge already keeps (by URL). - Such an override also keeps the response of a signed-in request: admin pages render the admin's data during SSR (
/zh/admin/users,/zh/admin/contacts), a draft preview renders the draft at its public URL and in its public API, and a site may render an account page. Cached once, the next visitor of that URL in the same data center gets it. A rule that only skips paths starting with/adminmisses/<locale>/admin.nuxtcms fleet cache-rules --shortaddsand not (...)to such rules: requests with a session cookie (auth_token,cms_preview,shop_member,saas_user,hub_session) and paths ending in/admin,/setupor containing/admin/are left to Cloudflare's default (not cached). Anonymous visitors are cached as before. /_i18n/<hash>/<locale>/messages.jsonis cached for a year (its URL changes with the message files).- Cache headers a site sets on pages in its own
routeRulesare left as they are.
Smaller cold starts: CF_PAGES_NO_BUNDLE=1
On a quiet site most renders run in a new isolate, which first compiles the worker. By default
wrangler pages deploy re-bundles Nitro's dist/_worker.js chunks (admin pages included) into one
~6 MB module that every isolate compiles. With CF_PAGES_NO_BUNDLE=1 in .cloudflare.env,
nuxtcms upgrade --deploy, fleet upgrade and pnpm run deploy add --no-bundle: the chunks are
uploaded as built and an isolate compiles only what a request imports (zbservice: ~120 ms -> ~50 ms
of compile at isolate startup; the CPU of the requests themselves was unchanged on local workerd).
Wrangler warns that nodejs_compat_v2 polyfills are not
applied; Nitro's output does not need them. wrangler pages dev --no-bundle does not start with
this output (use wrangler dev with no_bundle to try it locally).
2. Cloudflare Workers
- Copy
node_modules/nuxtcms/deploy/wrangler.worker.toml.exampletowrangler.tomland fill in the D1 / R2 ids. pnpm build:workerwrangler d1 migrations apply <db> --remotewrangler deploy
Workers and Pages use the same SQLite migration files. For PostgreSQL / MySQL on Workers, create a Hyperdrive, bind it as HYPERDRIVE and build with NUXT_DB_DRIVER=pg|mysql.
3. Linux server (including Baota)
pnpm install
NUXT_DB_DRIVER=mysql pnpm build:node # or sqlite / pg
DATABASE_URL=mysql://user:[email protected]:3306/nuxtcms NUXT_DB_AUTO_MIGRATE=1 pnpm start
- Process manager:
pm2 start ../../deploy/ecosystem.config.cjs --env production. Baota's "Node project" feature wraps PM2: set the start file to.output/server/index.mjsand put the environment variables in the project settings; Baota's bundled MySQL works directly asDATABASE_URL. - Reverse proxy:
node_modules/nuxtcms/deploy/nginx-node-proxy.conf.example; nginx serves/_nuxt/straight from disk. - Docker:
node_modules/nuxtcms/deploy/Dockerfile,--build-arg NUXT_DB_DRIVER=pgselects the driver. - Media: local disk (default
.data/uploads) orNUXT_STORAGE_DRIVER=oss|cos|s3|r2; see Configuration.
4. Split frontend / backend
The frontend is built as a static SPA (pnpm build:static, output in .output/public); the backend serves the API and the files it generates.
Recommended: one domain. nginx serves the static files and proxies /api/, robots.txt, sitemap.xml, /__sitemap__/, llms.txt, llms-full.txt and the IndexNow key file to the backend. Frontend and API share an origin: no CORS, no cross-site cookie, NUXT_PUBLIC_API_BASE stays empty. Config template: node_modules/nuxtcms/deploy/nginx-static-frontend.conf.example (verified on nginx 1.28 with a real site).
Backend (Pages, Workers or node) runtime variable:
NUXT_TRUSTED_PROXIES=<public IP of the nginx server> # rate limits and captcha see the visitor, not nginx
What the template takes care of:
- The SPA fallback is
200.html, notindex.html: the rootindex.htmlonly redirects to the default language. The build turns200.html/404.htmlinto real SPA shells; for output built by an earlier version runnode node_modules/nuxtcms/scripts/spa-fallback.mjs .output/public. robots.txtandsitemap.xmlprerendered at build time are not live; nginx always asks the backend.client_max_body_size 20m: nginx allows 1 MB by default, so image uploads would fail with 413.- HTML shells are
no-cache(a new deploy shows at once),/_nuxt/is cached for a year.
Two domains (frontend www.example.com, backend api.example.com, no proxy):
# backend runtime
NUXT_CORS_ORIGINS=https://www.example.com # allowed frontend origins, comma separated
NUXT_AUTH_COOKIE_SAME_SITE=none # cross-origin login cookie, HTTPS required
NUXT_STORAGE_PUBLIC_URL=https://cdn.example.com # media via CDN so relative paths do not hit the frontend host
# frontend build
NUXT_PUBLIC_API_BASE=https://api.example.com pnpm build:static
Use subdomains of one domain. Between unrelated domains (say example.cn and xxx.pages.dev) Safari and other browsers block the login cookie as a third-party cookie and the admin cannot sign in. robots.txt, the sitemap and llms.txt are then served by the backend domain.
Limitations:
- No server-side rendering: titles, descriptions and hreflang appear only after the browser runs JavaScript, so search engines and AI crawlers mostly see an empty page. Public sites that need SEO or AI search should use one of the first three targets, or the Hong Kong reverse proxy (section 6).
- Backend redirect rules (
routeRulesredirects in the site config, domain and path rules in Admin → Redirects) do not apply to static pages;**rules are not expanded in the browser. - Third-party services that allow listed origins only (chat widgets, analytics) need the new frontend domain.
/docs(Nuxt Content) queries content on the client.- The browser loads every piece of data itself, so a shared
useAsyncDatathat several components call at once (site settings, menus) needsdedupe: 'defer': with Nuxt's default'cancel'each caller restarts the request (SSR hides this behind the payload). The base composables do this; check copies in your site (useSite, your own site-settings composable) and give data for the same URL the same key.
5. Serverless and other clouds
Serverless platforms have no persistent disk, so:
- use a remote database: Turso (libSQL,
DATABASE_URL=libsql://xxx.turso.io+DATABASE_AUTH_TOKEN, same migrations as D1), PostgreSQL (Neon, Supabase, RDS; build withNUXT_DB_DRIVER=pg) or MySQL (PlanetScale, TiDB Cloud, RDS;NUXT_DB_DRIVER=mysql); - store media in object storage:
NUXT_STORAGE_DRIVER=s3|oss|cos|r2; - the API response cache switches to memory and build-time prerendering is skipped automatically;
- cron: point the platform scheduler (Vercel Cron, Netlify Scheduled Functions, EventBridge...) or
deploy/cron-workeratPOST /api/cron/dispatch.
NUXT_DEPLOY_TARGET=vercel NUXT_DB_DRIVER=pg pnpm build
Alibaba Cloud FC / Tencent SCF: build the image from deploy/Dockerfile and deploy it as a custom container listening on port 3000.
6. Mainland China access (Hong Kong reverse proxy)
Visitors in mainland China reach Cloudflare over international routes that are often slow at peak hours. Without an ICP filing, put an nginx server in Hong Kong (or Singapore / Japan) in front of the site: mainland visitor → Hong Kong server → Cloudflare Pages. The site itself stays as it is.
- Server: a small Hong Kong VM with a CN2 GIA / China-optimised route, with nginx.
- nginx: copy
deploy/nginx-hk-proxy.conf.example, replacewww.example.cnwith the domain andyour-project.pages.devwith the Pages project. Build assets under/_nuxt/are cached on the proxy; pages and APIs always go to the origin. - DNS, either:
- everything to the Hong Kong server (simplest; visitors abroad take one extra hop), or
- split resolution (DNSPod / Alibaba Cloud DNS "mainland / overseas" lines): the mainland line points at the proxy, the overseas line keeps the CNAME to Cloudflare. The domain's name servers must then be at DNSPod or Alibaba Cloud.
- Certificate: the proxy needs its own HTTPS certificate (certbot, acme.sh). With split DNS use the DNS-01 challenge: Let's Encrypt validates from abroad and would reach Cloudflare.
- Visitor IP: set
NUXT_TRUSTED_PROXIES=<proxy public IP>(comma-separated, IPv4 ranges allowed) as a Pages environment variable and redeploy. Without it every mainland visitor has the proxy's IP for login limits, captcha and payment requests, so one person mistyping a password can lock everyone out. Only requests from these addresses have theirX-Forwarded-Forhonoured; anyone else forging the header gets nothing.
Check from a mainland network: /_nuxt/ files answer X-Proxy-Cache: HIT, and admin login,
uploads and checkout work.
Variable cheat sheet
| Variable | Stage | Purpose |
|---|---|---|
NUXT_DEPLOY_TARGET | build | deployment shape |
NUXT_DB_DRIVER | build | sqlite / pg / mysql |
DATABASE_AUTH_TOKEN | runtime | token for remote libSQL (Turso) |
DATABASE_URL | runtime | connection string (ignored on Cloudflare, bindings are used) |
NUXT_DB_AUTO_MIGRATE | runtime | 1 = run migrations at startup (node target) |
NUXT_STORAGE_* | runtime | media storage driver and credentials |
NUXT_PUBLIC_API_BASE | build | backend URL for a static frontend |
NUXT_CORS_ORIGINS | runtime | allowed frontend origins |
NUXT_AUTH_COOKIE_SAME_SITE | runtime | none for cross-origin setups |
NUXT_TRUSTED_PROXIES | runtime | reverse proxy IPs in front of the site (e.g. Hong Kong nginx); their forwarded visitor IP is used |
NUXT_CF_PURGE_API_TOKEN | runtime | purge the site's Cloudflare cache on public changes and keep pages at the edge for a day (Pages secret) |
NUXT_CF_PURGE_ZONE_ID / NUXT_CF_PURGE_HOSTS | runtime | optional: zone and host names to purge |
NUXT_PAGE_CACHE_PURGED_MAX_AGE / NUXT_PAGE_CACHE_PURGED_SWR | runtime | page edge TTL with a purge token (default 86400 / 604800 s) |
NUXT_DEMO_MODE | build | true = public demo site (see below). Never on a real site |
Public demo site
NUXT_DEMO_MODE=true (at build time) turns a site into a public demo, such as the shop demo on
the InsightSite website:
- a notice bar on every page with an "Try the admin" button; the login page gets a one-click
"Enter the demo admin" button. Both sign in as a shared demo admin through
/api/auth/demo(no password). - admin actions that could leak secrets, run code or abuse the site are refused: users and passwords, e-mail settings, security / captcha, site settings, plugins (install, settings, hub), backups, uploads, scheduled tasks, translations and languages. Contact messages, newsletter subscribers and exports are not readable. HTML saved through the admin is stripped of scripts, frames, forms and event handlers.
- all data is restored once a day (first request after midnight,
NUXT_CRON_TZ_OFFSETtime): content, users, contact messages and the demo content set. Package plugins restore their own data through thenuxtcms:demo-resetNitro hook (the shop restores demo prices and coupons and deletes the orders). An empty database is filled on the first request. - shop: WeChat Pay and Alipay are offered without credentials (CNY) and are simulated: the order page shows a QR code (it opens the order page on a phone) and a "simulate a successful payment" button. Checkout is pre-filled with sample details.