Skip to content

Nuxt

Hetzner.Website.Nuxt deploys a Nuxt app to a Hetzner Cloud Server. It builds the app through your project’s own @nuxt/kit with nitro’s node preset: the nitro Node server runs as a systemd unit on port 3000, and client assets plus prerendered pages are baked into the unit. Omit server to get a cpx12 / ubuntu-24.04 VM in fsn1. The live URL is http://{ipv4}:3000 — the Service has no TLS. There is no nitro.preset to edit and no build command to run.

The build integration is not bundled with alchemy. Install @alchemy.run/frontend-frameworks; the resource loads its /nuxt and /nuxt/node exports from your project at deploy time. It is only used at build time, so a dev dependency is enough:

Terminal window
bun add -d @alchemy.run/frontend-frameworks

Your nuxt.config.ts loads natively — modules, layers, and all — so configure Nuxt exactly as you would outside Alchemy:

nuxt.config.ts
export default defineNuxtConfig({
modules: ["@nuxtjs/tailwindcss"],
routeRules: {
"/about": { prerender: true },
},
});

Deploy-specific overrides are merged over it via the nuxt prop (the override wins) — see Prerendering below for an example.

Don’t set nitro.preset — the Node deploy target owns the preset ("node"), and a foreign preset is a hard error.

Declare the site as a module-level const (rather than inline in the Stack):

alchemy.run.ts
import * as Hetzner from "alchemy/Hetzner";
export const Website = Hetzner.Website.Nuxt("Website");

Pass rootDir if package.json is not at .. Pass server to run the unit on a Server you already declared — several sites and APIs can share one VM. Otherwise the site creates a cpx12 / ubuntu-24.04 Server in fsn1.

export const Box = Hetzner.Server("Box", {
serverType: "cpx12",
image: "ubuntu-24.04",
location: "fsn1",
});
export const Website = Hetzner.Website.Nuxt("Website", {
server: Box,
});

Yield the site from your Stack and return its URL:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
export default Alchemy.Stack(
"MyNuxtSite",
{
providers: Hetzner.providers(),
state: Alchemy.localState(),
},
Effect.gen(function* () {
const site = yield* Website;
return { url: site.url };
}),
);

On deploy site.url is http://{ipv4}:3000 (or http://{domain}:3000 when you set domain). site.server and site.service are undefined under alchemy dev.

Process environment is the top-level env prop — plain strings, or Redacted values. They are copied onto process.env before build and alchemy dev, and onto the systemd unit at deploy:

alchemy.run.ts
export const Website = Hetzner.Website.Nuxt("Website", {
env: {
GREETING: "Hello from Alchemy!",
NUXT_PUBLIC_API_BASE: "https://api.example.com",
},
});

This is the hosted Node process, not Worker bindings. NUXT_-prefixed keys override Nuxt runtimeConfig the usual way; NUXT_PUBLIC_ values are inlined into the client at build time.

The nitro server is a plain Node process, so server routes and SSR read the environment from process.env:

server/api/hello.ts
export default defineEventHandler(() => {
return { greeting: process.env.GREETING ?? "hello" };
});

Routes marked for prerendering in routeRules (or via nitro.prerender) render at build time into .output/public and are baked into the unit, served as static files before the nitro handler:

export const Website = Hetzner.Website.Nuxt("Website", {
nuxt: {
routeRules: {
"/about": { prerender: true },
},
},
});

Nitro’s isr route rule is implemented only by the Vercel and Netlify presets. On the Node preset it is silently ignored at build time, and the route renders on demand like any other SSR route. Use prerender for build-time static routes, or cache route rules for runtime caching.

Terminal window
bun alchemy dev

alchemy dev runs Nuxt’s own dev server (nitro dev, full HMR) instead of deploying; site.url is the local address and no Hetzner resources are created. Wrap the site in Alchemy.remote() to deploy the live Server and Service even during dev.

domain requires an existing Hetzner.Zone — the Website does not create a zone. It adds an A RecordSet pointing at the Server’s public IPv4, and url becomes http://{domain}:3000. There is no TLS on the Service:

export const Dns = Hetzner.Zone("Dns", {
name: "example.com",
});
export const Website = Hetzner.Website.Nuxt("Website", {
domain: "app.example.com",
zone: Dns,
});

Passing domain without zone fails.