1---
2title: Next.js
3description: "Deploy a Next.js app to Hetzner with Hetzner.Website.Nextjs — next build plus a Node next({ dev: false }) systemd unit, and next dev under alchemy dev."
4sidebar:
5 order: 9
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12`Hetzner.Website.Nextjs` deploys a [Next.js](https://nextjs.org) app
13to a Hetzner Cloud Server. It runs a real `next build`, then a
14long-running `next({ dev: false })` systemd unit on port 3000:
15
16- one [`Hetzner.Server`](/hetzner/compute/servers) (default `cpx12`
17 / `ubuntu-24.04` / `fsn1` when `server` is omitted) and a
18 [`Hetzner.Service`](/hetzner/compute/services) as a systemd unit
19- live URL `http://{ipv4}:3000`
20- `.next` and `public/` packed into the unit archive; `next`,
21 `react`, and `react-dom` installed with `bun install`
22
23This is **not** OpenNext — those wrappers target Lambda and workerd.
24ISR and `next/image` use Next's own Node behavior. Several sites and
25APIs can share one Server.
26
27App Router and Pages Router both work — server components, API
28routes, middleware, server actions, dynamic segments, streaming SSR,
29and `getServerSideProps` pages all run in the Node process.
30
31## Install
32
33The build integration is not bundled with alchemy. Install
34`@alchemy.run/frontend-frameworks`; the resource loads the package's
35`/nextjs/node` export from your project at deploy time. It is only
36used at build time, so a dev dependency is enough. Your app still
37needs `next`, `react`, and `react-dom` as usual:
38
39<Tabs syncKey="pkgManager">
40 <TabItem label="bun" icon="bun">
41 <Code code={`bun add -d ${devDeps}`} lang="sh" />
42 </TabItem>
43 <TabItem label="npm" icon="npm">
44 <Code code={`npm install -D ${devDeps}`} lang="sh" />
45 </TabItem>
46 <TabItem label="pnpm" icon="pnpm">
47 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
48 </TabItem>
49 <TabItem label="yarn" icon="seti:yarn">
50 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
51 </TabItem>
52</Tabs>
53
54## Configure Next.js
55
56Your `next.config.*` is loaded and honored as-is — the build runs a
57real `next build`, and Alchemy never rewrites the file. There is no
58adapter to install and no `open-next.config.ts` to write.
59
60Pass `rootDir` when the Next.js project is not the stack root.
61
62## Declare the Website
63
64Declare the site as a module-level const (rather than inline in the
65Stack):
66
67```typescript
68// alchemy.run.ts
69import * as Hetzner from "alchemy/Hetzner";
70
71export const Website = Hetzner.Website.Nextjs("Website");
72```
73
74## Add it to the Stack
75
76Yield the site from your Stack and return its URL:
77
78```typescript
79// alchemy.run.ts
80import * as Alchemy from "alchemy";
81import * as Effect from "effect/Effect";
82
83export default Alchemy.Stack(
84 "MyNextjsSite",
85 {
86 providers: Hetzner.providers(),
87 state: Alchemy.localState(),
88 },
89 Effect.gen(function* () {
90 const site = yield* Website;
91 return { url: site.url };
92 }),
93);
94```
95
96`url` is `http://{ipv4}:3000` on deploy. `server` and `service` are
97set then and `undefined` under `alchemy dev`.
98
99Pass an existing Server so the Next unit shares the VM with other
100Services:
101
102```typescript
103export const Website = Hetzner.Website.Nextjs("Website", {
104 server: Box,
105});
106```
107
108## Add environment variables
109
110Process environment is the top-level `env` prop — plain strings, or
111`Redacted` secrets:
112
113```diff lang="typescript"
114// alchemy.run.ts
115export const Website = Hetzner.Website.Nextjs("Website", {
116+ env: {
117+ GREETING: "Hello from Alchemy!",
118+ API_BASE: "https://api.example.com",
119+ },
120});
121```
122
123The values are copied onto `process.env` before `next build` /
124`next dev`, and onto the systemd unit at deploy, so server code and
125`NEXT_PUBLIC_*` inlining see the same map in both modes.
126
127## Read the environment in server code
128
129The Next server is a plain Node process, so route handlers, server
130components, and server actions read the environment from
131`process.env`:
132
133```typescript
134// app/api/hello/route.ts
135export function GET() {
136 return Response.json({ greeting: process.env.GREETING ?? "hello" });
137}
138```
139
140## Local dev
141
142```sh
143bun alchemy dev
144```
145
146`alchemy dev` runs `next dev` (native HMR, Turbopack) instead of
147deploying; `site.url` is the local address and no Hetzner resources
148are created. Wrap the site in `Alchemy.remote()` to deploy the live
149Service even during dev.
150
151## Custom domain
152
153```typescript
154const site = yield* Hetzner.Website.Nextjs("Web", {
155 domain: "app.example.com",
156 zone,
157});
158```
159
160`domain` requires an existing [`Hetzner.Zone`](/hetzner/networking/dns)
161— v1 does not create one, and passing `domain` without `zone` fails.
162Alchemy creates an A RecordSet pointing at the Server's public IPv4.
163`url` becomes `http://app.example.com:3000`. There is no TLS on the
164Service.
165
166## Where next
167
168- [`Hetzner.Website.Nextjs` reference](/providers/hetzner/website/nextjs)
169 — every prop and attribute
170- [Servers](/hetzner/compute/servers),
171 [Services](/hetzner/compute/services),
172 [DNS](/hetzner/networking/dns)