Next.js
Hetzner.Website.Nextjs deploys a Next.js app
to a Hetzner Cloud Server. It runs a real next build, then a
long-running next({ dev: false }) systemd unit on port 3000:
- one
Hetzner.Server(defaultcpx12/ubuntu-24.04/fsn1whenserveris omitted) and aHetzner.Serviceas a systemd unit - live URL
http://{ipv4}:3000 .nextandpublic/packed into the unit archive;next,react, andreact-dominstalled withbun install
This is not OpenNext — those wrappers target Lambda and workerd.
ISR and next/image use Next’s own Node behavior. Several sites and
APIs can share one Server.
App Router and Pages Router both work — server components, API
routes, middleware, server actions, dynamic segments, streaming SSR,
and getServerSideProps pages all run in the Node process.
Install
Section titled “Install”The build integration is not bundled with alchemy. Install
@alchemy.run/frontend-frameworks; the resource loads the package’s
/nextjs/node export from your project at deploy time. It is only
used at build time, so a dev dependency is enough. Your app still
needs next, react, and react-dom as usual:
bun add -d @alchemy.run/frontend-frameworksnpm install -D @alchemy.run/frontend-frameworkspnpm add -D @alchemy.run/frontend-frameworksyarn add -D @alchemy.run/frontend-frameworksConfigure Next.js
Section titled “Configure Next.js”Your next.config.* is loaded and honored as-is — the build runs a
real next build, and Alchemy never rewrites the file. There is no
adapter to install and no open-next.config.ts to write.
Pass rootDir when the Next.js project is not the stack root.
Declare the Website
Section titled “Declare the Website”Declare the site as a module-level const (rather than inline in the Stack):
import * as Hetzner from "alchemy/Hetzner";
export const Website = Hetzner.Website.Nextjs("Website");Add it to the Stack
Section titled “Add it to the Stack”Yield the site from your Stack and return its URL:
import * as Alchemy from "alchemy";import * as Effect from "effect/Effect";
export default Alchemy.Stack( "MyNextjsSite", { providers: Hetzner.providers(), state: Alchemy.localState(), }, Effect.gen(function* () { const site = yield* Website; return { url: site.url }; }),);url is http://{ipv4}:3000 on deploy. server and service are
set then and undefined under alchemy dev.
Pass an existing Server so the Next unit shares the VM with other Services:
export const Website = Hetzner.Website.Nextjs("Website", { server: Box,});Add environment variables
Section titled “Add environment variables”Process environment is the top-level env prop — plain strings, or
Redacted secrets:
export const Website = Hetzner.Website.Nextjs("Website", { env: { GREETING: "Hello from Alchemy!", API_BASE: "https://api.example.com", },});The values are copied onto process.env before next build /
next dev, and onto the systemd unit at deploy, so server code and
NEXT_PUBLIC_* inlining see the same map in both modes.
Read the environment in server code
Section titled “Read the environment in server code”The Next server is a plain Node process, so route handlers, server
components, and server actions read the environment from
process.env:
export function GET() { return Response.json({ greeting: process.env.GREETING ?? "hello" });}Local dev
Section titled “Local dev”bun alchemy devalchemy dev runs next dev (native HMR, Turbopack) 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
Service even during dev.
Custom domain
Section titled “Custom domain”const site = yield* Hetzner.Website.Nextjs("Web", { domain: "app.example.com", zone,});domain requires an existing Hetzner.Zone
— v1 does not create one, and passing domain without zone fails.
Alchemy creates an A RecordSet pointing at the Server’s public IPv4.
url becomes http://app.example.com:3000. There is no TLS on the
Service.
Where next
Section titled “Where next”Hetzner.Website.Nextjsreference — every prop and attribute- Servers, Services, DNS