all posts

The best Nuxt hosting platforms in 2026

Ajay Kumar··9 min read

Nuxt is unusual among frameworks in that the hosting decision is made by the build, not by the platform. Nitro — the server engine underneath Nuxt 3 and 4 — compiles your app for a specific target, and that target decides what `nuxt build` even emits. The same repository can produce a folder of static HTML, a standalone Node server, a bundle of serverless functions, or a Cloudflare Worker, without one line of application code changing.

Which is wonderful right up until you deploy the wrong shape somewhere and spend an afternoon reading stack traces about a missing `.output/server/index.mjs` that was never going to exist. So this roundup is organised the way the decision actually works: preset first, deploy shape second, platform names last. I build PandaStack, which is one of the options below — I'll flag where it fits and where it doesn't.

Start with the preset, because it decides everything else

Nitro ships presets for the platforms people actually deploy to: `node-server` (the default), `static`, plus platform-specific ones for Vercel, Netlify, Cloudflare, Deno, AWS Lambda, Bun and more. You select one either in `nuxt.config.ts` under `nitro.preset`, or with the `NITRO_PRESET` environment variable at build time. Nitro also auto-detects several platforms from their CI environment variables, which is convenient and occasionally confusing — a build that picks a preset for you is a build whose output shape you didn't choose.

The practical consequence: the artifact differs per preset. `node-server` gives you `.output/server/index.mjs` and `.output/public/`. `static` gives you `.output/public/` and no server at all. The Vercel and Netlify presets emit that platform's function directory layout. Cloudflare emits a Worker. If your host expects one and your build produced another, the failure looks like a broken app and is really a mismatched target.

The single most common Nuxt deploy failure is a preset mismatch: a platform's build detection sets one preset while your start command assumes another. If your logs say the entry file doesn't exist, don't debug your code — print the contents of `.output/` in the build step and see what Nitro actually produced.

The three deploy shapes, and what each one costs

Fully static — nuxt generate

`nuxt generate` prerenders your routes at build time and emits plain files. Deploy them to any static host or object store and the operational burden is close to zero: nothing to keep alive, nothing to scale, no cold starts, and a CDN edge cache that just works. For docs, marketing sites, blogs and brochureware this is the correct answer and everything below is overkill.

What you give up is anything that has to happen per request. No `server/api` routes at runtime, no reading cookies on the server, no per-user rendering, no revalidation short of a full rebuild. Data must come from a separate API and be fetched in the browser, or be baked in at generate time and go stale on your schedule.

SSR on a long-running Node server

The `node-server` preset builds `.output/server/index.mjs`, an ordinary Node process you start and keep alive. Every Nuxt feature works, because nothing is being translated: server routes, server middleware, streaming responses, WebSockets, in-process caches, native modules, a conventional Postgres driver over TCP. There's no adapter between your code and the runtime to lag behind a Nuxt release.

What you take on: a process to supervise, a CDN in front for assets (Nitro will happily serve `.output/public` from Node, which works and burns event-loop time you'd rather spend on requests), and your own scaling decisions. In exchange the output is genuinely portable — the same build runs on a PaaS, a microVM, a VPS, or your laptop.

SSR on serverless or edge

Platform presets compile your Nuxt app into that platform's function primitive. You get scale-out for free, no process to supervise, and typically per-invocation billing that's very cheap when traffic is low and less cheap when it isn't. The trade-offs are the usual serverless ones plus a runtime question: cold starts on infrequently-hit routes; per-invocation billing that includes crawlers and uptime monitors; no in-memory state between requests, so any cache has to be external.

Edge runtimes add a sharper constraint. Cloudflare Workers, Vercel's edge runtime and Deno all run a V8 isolate rather than Node. Node compatibility has improved a great deal and a plain Nuxt app usually runs unmodified, but native modules, filesystem access and TCP database drivers are still where it breaks. Verify your database driver on the target runtime early — that's the dealbreaker that surfaces at request time rather than build time. Streaming and WebSockets also vary by platform; if you rely on either, test them specifically rather than assuming.

// nuxt.config.ts — the file that decides your hosting
export default defineNuxtConfig({
  nitro: {
    // Omit this and Nitro auto-detects from CI env vars.
    // Set it explicitly when you want a predictable artifact.
    preset: "node-server",
  },

  // Hybrid rendering: per-route, not per-app.
  routeRules: {
    "/": { prerender: true },                  // built once, served as HTML
    "/blog/**": { isr: 3600 },                 // platform-specific support
    "/pricing": { swr: 600 },                  // stale-while-revalidate
    "/app/**": { ssr: false },                 // client-only SPA island
    "/api/**": { cors: true },
    "/old-path": { redirect: "/new-path" },
  },

  runtimeConfig: {
    // Server-only. Populated from NUXT_STRIPE_SECRET at runtime.
    stripeSecret: "",
    // Exposed to the browser. From NUXT_PUBLIC_API_BASE.
    public: {
      apiBase: "",
    },
  },
});

Hybrid rendering is per-route, and support is per-platform

`routeRules` is Nuxt's best feature and its most portable trap. Marking a route `prerender: true` works everywhere, because prerendering happens during the build and produces a file. `ssr: false` works everywhere, because it just means don't render this route on the server. Those two are safe.

`isr` and `swr` are different. They describe caching behaviour that something has to implement, and the something is the platform. On the presets for platforms with native incremental rendering, `isr` maps onto that primitive. On a Node server, `swr` is served from Nitro's own cache storage — which by default means the memory of the process that handled the request, so with several instances behind a load balancer, each one caches independently and users see different versions until every instance catches up. The fix is to point Nitro's cache storage at shared storage such as Redis. On edge presets the semantics differ again.

None of this is a bug. It's just that caching is a distributed-systems feature wearing a one-line config disguise. Before you rely on `isr` or `swr`, read your target preset's documentation for those specific rules, then test it: revalidate, reload a dozen times, and see whether the content flip-flops.

server/api is real backend code

Anything in `server/api/**` and `server/routes/**` is a backend. It holds your secrets, it talks to your database, and it needs somewhere to execute. This is worth stating plainly because a lot of Nuxt apps drift into having a substantial backend without anyone deciding to build one, and then the team tries to deploy to a static host and discovers half the app doesn't exist.

If you have server routes, your options are: run them on a Node server, run them as functions on a serverless platform, or run them on an edge runtime with the compatibility caveats above. Static hosting is off the table for those routes, though a hybrid build — prerender the marketing pages, SSR the app — is a perfectly good middle ground and exactly what `routeRules` is for.

runtimeConfig, and the classic 'it worked locally' bug

Nuxt's `runtimeConfig` maps environment variables to config keys at runtime, with a naming convention: `NUXT_STRIPE_SECRET` fills `runtimeConfig.stripeSecret`, and `NUXT_PUBLIC_API_BASE` fills `runtimeConfig.public.apiBase`. The rule people trip over is that the key must already be declared in `nuxt.config.ts`. If it isn't there, the environment variable is ignored entirely — no warning, no error, just an empty string where your API key should be.

The second trap is timing. On an SSR deployment, runtime config is genuinely read at runtime, which is what you want. On a prerendered or fully static build, whatever was in the environment during the build is baked into the generated output — so rotating a value means rebuilding. Anything under `public` is serialized into the page payload and reaches the browser, so it is not a place for secrets under any circumstances.

Debugging a missing value: log `useRuntimeConfig()` server-side on a deployed request, not locally. Nine times out of ten you'll find either an undeclared key in nuxt.config.ts, or a variable that exists in your platform's runtime environment but was never exposed to the build step that needed it.

Node versions, pnpm, and builds that die quietly

Three build-side things decide whether your deploy is boring or a Thursday afternoon. Pin your Node version in the repository — `.nvmrc` or `.tool-versions` — rather than in a dashboard dropdown, so the same version builds locally, in CI and on the platform. Commit your lockfile and make sure the platform uses the matching package manager; a pnpm repo installed with npm will resolve a different dependency tree, and Nuxt's module ecosystem is exactly the kind of place where that turns into a mystery.

And budget build memory. A large Nuxt app doing type checking, bundling and prerendering in one pass will happily ask for more than a small builder has. The symptom is famously unhelpful: the build stops, says `killed` or exit code 137, and offers no stack trace. Options in order: build somewhere with a bigger ceiling, raise Node's heap via `NODE_OPTIONS=--max-old-space-size`, prerender fewer routes at build time, or move type checking into CI so the deploy build doesn't do it twice.

The platforms, honestly

Qualitative only — every platform's pricing, regions and free tiers move faster than any blog post. Verify against their current docs and pricing pages before you commit anything with a credit card attached.

  • Vercel — model: serverless/edge functions plus CDN, first-party Nitro preset. Nuxt fit: excellent; hybrid rendering, ISR and image optimization are well-trodden here and the zero-config path really is zero-config. Caveat: metered bandwidth and invocations mean traffic spikes and crawler traffic show up on the bill, so price your real numbers.
  • Netlify — model: serverless functions plus a mature static CDN, first-party Nitro preset. Nuxt fit: very strong for content-heavy hybrid sites; prerendering plus a few server routes is its sweet spot. Caveat: same metered-usage shape as Vercel, and long-running or streaming work is not what functions are for.
  • Cloudflare Pages / Workers — model: V8 isolates at the edge, Nitro cloudflare presets. Nuxt fit: very fast and very cheap when your dependencies fit the runtime, with a good story for static assets plus dynamic routes. Caveat: it isn't Node — check native modules, filesystem use and especially your database driver before you're committed.
  • NuxtHub — model: a Nuxt-native layer over Cloudflare, with database, KV, blob and cache wired into the framework. Nuxt fit: the most Nuxt-shaped developer experience available; the primitives are the ones the framework already expects. Caveat: you inherit the Workers runtime constraints, and you're adopting an opinionated stack that's harder to leave than a plain Node server.
  • Render — model: container PaaS running the node-server output. Nuxt fit: everything works because it's just Node; predictable pricing, straightforward git deploys. Caveat: you supply the CDN and the image pipeline, and you should check the build instance's memory ceiling against your app.
  • Railway — model: container PaaS with strong DX and usage-based pricing. Nuxt fit: same as Render — node-server, no adapter, no feature asterisks, plus easy managed databases alongside. Caveat: usage-based billing rewards attention; an always-on instance you forgot about is a line item.
  • Fly.io — model: microVMs, multi-region, run the node-server output close to users. Nuxt fit: excellent for global SSR with a real Node runtime, and it can idle machines down. Caveat: multi-region is genuinely powerful and genuinely more to reason about — especially your database's location relative to your app's.
  • Plain VPS with Docker or systemd — model: you own the box. Nuxt fit: perfect, technically; `node .output/server/index.mjs` behind a reverse proxy is a solved problem. Caveat: you now own TLS renewal, restarts, log rotation, OS patching and the pager. Fine for a side project, a real decision for a team.
  • PandaStack (mine) — model: git-push deploys onto a Firecracker microVM running the Nitro node-server output, with framework auto-detection, `.nvmrc`/mise runtime pinning, scale-to-zero when idle, and managed Postgres on the same substrate. Nuxt fit: full Node SSR with hardware isolation per app; a fresh microVM restores in about 179ms at p50 (203ms p99), so waking from zero is fast rather than a provisioning step. Caveat: it's a smaller, younger platform than the incumbents — fewer regions, no built-in image-optimization CDN, and you'll want Cloudflare in front for global asset caching.

Deploying Nuxt SSR on a Node server

This is the shape with the fewest asterisks, and it's the same three commands everywhere. Build, then run the emitted entry point, and let the platform tell the process which port and interface to bind.

# 1. Build for a long-running Node process
NITRO_PRESET=node-server npm run build

# 2. Look at what you actually got before deploying anything
ls -R .output | head -30
#   .output/public/         <- static assets, hand these to a CDN
#   .output/server/index.mjs <- the server you start

# 3. Run it. Nitro reads PORT and HOST from the environment.
PORT=3000 HOST=0.0.0.0 node .output/server/index.mjs

# HOST=0.0.0.0 matters: bound to 127.0.0.1 the process is healthy,
# passes no health check, and receives no traffic. Classic.

# Runtime config comes from NUXT_* env vars at start time
NUXT_STRIPE_SECRET=sk_live_... \
NUXT_PUBLIC_API_BASE=https://api.example.com \
PORT=3000 HOST=0.0.0.0 node .output/server/index.mjs

For a git-driven platform, the same thing becomes two settings plus a pinned runtime. Put the version in the repo so it travels with the code:

# Pin the runtime in the repo, not in a dashboard dropdown
echo "22.11.0" > .nvmrc

# or, if you prefer mise / asdf style pinning:
cat > .tool-versions <<'EOF'
nodejs 22.11.0
EOF

# Then the platform only needs two commands:
#   build:  npm ci && npm run build
#   start:  node .output/server/index.mjs
#
# The start command must bind the port the platform gives you.
# On PandaStack that's $PORT, injected into the app's environment:
#   start:  PORT=$PORT HOST=0.0.0.0 node .output/server/index.mjs

# Sanity-check the exact build command locally first
npm ci && npm run build && node .output/server/index.mjs

Pick by situation

  • If your site is docs, a blog, or marketing with no per-request logic → `nuxt generate` onto any static host or CDN. Stop reading, you're done.
  • If you're mostly static with a handful of server routes and want zero ops → Vercel or Netlify with the first-party preset, and watch the metered usage.
  • If your dependencies fit a V8 isolate and you want global latency cheaply → Cloudflare Pages/Workers, after you've verified your database driver.
  • If you want the most Nuxt-native full-stack experience and are happy on Cloudflare → NuxtHub.
  • If you have a conventional backend — TCP Postgres, native modules, background work, WebSockets → the node-server preset on a container PaaS such as Render or Railway.
  • If you need global SSR close to users with a real Node runtime → Fly.io, and think hard about where your database lives.
  • If the app is idle most of the time and you don't want to pay for that → a scale-to-zero platform, PandaStack included; check that bots and uptime monitors aren't keeping it awake.
  • If you need hardware-level isolation per app, or your app runs code it didn't write → a microVM platform rather than a shared-kernel container.
  • If you enjoy operating servers and want maximum control per dollar → a VPS with Docker or systemd and a reverse proxy.
  • If you're already deployed and it works and the bill is fine → stay. 'Migrate to the thing from a blog post' is rarely the highest-value ticket.

The short version

Nuxt's hosting question has a boring answer once you ask it in the right order. Decide what has to happen per request; that picks your deploy shape. The deploy shape picks your Nitro preset. The preset narrows the platform list to a handful, and at that point you're choosing on price, region and taste rather than on capability.

The one recommendation I'll make unconditionally: whatever you pick, set `nitro.preset` explicitly rather than relying on auto-detection, and print `.output/` in your first build. Knowing exactly what artifact your build produces is worth more than any platform comparison, including this one.

Frequently asked questions

Can you deploy Nuxt without Docker?

Yes, and for most Nuxt apps Docker is optional rather than expected. The `node-server` preset emits `.output/server/index.mjs`, an ordinary Node process, so anything that can run `npm ci && npm run build` and then `node .output/server/index.mjs` can host it — a git-driven PaaS, a microVM platform, or a VPS with systemd and a reverse proxy. Pin your Node version with `.nvmrc` or `.tool-versions` so the build is reproducible, and make sure the start command binds the port and interface the platform expects. Docker becomes worth it when you have system-level dependencies, not merely because you're deploying.

What's the difference between nuxt build and nuxt generate?

`nuxt build` produces a server: with the default `node-server` preset you get `.output/server/index.mjs` plus static assets, and pages are rendered per request unless a route rule says otherwise. `nuxt generate` prerenders your routes at build time and produces `.output/public` — plain files, no server, deployable to any static host. The distinction that matters operationally is that generate bakes environment values and data into the output, so changing either means rebuilding, and your `server/api` routes have nowhere to run at request time. Hybrid builds via `routeRules` let you prerender some routes and server-render others from a single build.

Does Nuxt SSR work on the edge?

It does, through the Cloudflare, Vercel edge and Deno presets, and Node compatibility on those runtimes is much better than it used to be — a straightforward Nuxt app often runs unmodified. The constraints are in your dependencies rather than in Nuxt: native modules, filesystem access and conventional TCP database drivers are the usual blockers, so you'd use an HTTP or WebSocket-based database client instead. Streaming and WebSocket support also varies by platform. The reliable approach is to deploy early with your real data layer attached, because incompatibilities show up at request time, not at build time.

Why do my Nuxt env vars work locally but not in production?

Usually one of two reasons. First, the key isn't declared in `runtimeConfig` in `nuxt.config.ts` — Nuxt only maps `NUXT_*` environment variables onto keys that already exist there, and silently ignores the rest, so you get an empty string rather than an error. Second, timing: on a prerendered or statically generated build the values are baked in during the build, so a variable your platform only injects at runtime was never visible. Log `useRuntimeConfig()` from a deployed server request to see what the process actually has, and remember anything under `public` ships to the browser.

Do I need a CDN in front of a Nuxt Node server?

It works without one, and you'll want one at any real traffic level. Nitro will serve `.output/public` from the Node process, which means hashed JavaScript chunks, fonts and images consume event-loop time that should be handling render requests. Putting a CDN in front moves that traffic off the app entirely, adds edge caching for prerendered routes, gives you a sensible place to terminate TLS, and absorbs traffic spikes your single process would otherwise eat. It also helps with the other Nuxt performance lever worth attention: the hydration payload, which grows quietly as `useAsyncData` results are serialized into every page.

Keep reading

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.