1---
2title: Waku
3description: Deploy a Waku app to Hetzner with Hetzner.Website.Waku — RSC server as a systemd unit on port 3000, SSG pages baked into the unit, and Waku's own dev server under alchemy dev.
4sidebar:
5 order: 13
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12`Hetzner.Website.Waku` deploys a [Waku](https://waku.gg) app (React
13Server Components) to a Hetzner Cloud Server. It builds the project
14programmatically: the RSC server runs as a systemd
15[`Hetzner.Service`](/hetzner/compute/services) on port 3000, and the
16client output — including SSG-prerendered pages — is baked into the
17unit. Omit `server` and Alchemy creates a `cpx12` Ubuntu 24.04 box
18in `fsn1`. The live URL is `http://{ipv4}:3000` — the Service has
19no TLS. No `waku.config.ts` edits are required — if you have one it
20loads natively — and there is no adapter to configure and no build
21command to run.
22
23## Install
24
25The build integration is not bundled with alchemy. Install
26`@alchemy.run/frontend-frameworks`; the resource loads its `/waku`
27and `/waku/node` exports from your project at deploy time. It is only
28used at build time, so a dev dependency is enough:
29
30<Tabs syncKey="pkgManager">
31 <TabItem label="bun" icon="bun">
32 <Code code={`bun add -d ${devDeps}`} lang="sh" />
33 </TabItem>
34 <TabItem label="npm" icon="npm">
35 <Code code={`npm install -D ${devDeps}`} lang="sh" />
36 </TabItem>
37 <TabItem label="pnpm" icon="pnpm">
38 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
39 </TabItem>
40 <TabItem label="yarn" icon="seti:yarn">
41 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
42 </TabItem>
43</Tabs>
44
45## Configure Waku
46
47No framework config is required — a fresh Waku project deploys
48as-is. If your project has a `waku.config.ts`, Alchemy loads it
49natively (exactly as Waku's own CLI does) and uses it as the base
50config — including Vite plugins under its `vite` field:
51
52```typescript
53// waku.config.ts
54import tailwindcss from "@tailwindcss/vite";
55import { defineConfig } from "waku/config";
56
57export default defineConfig({
58 basePath: "/",
59 vite: {
60 plugins: [tailwindcss()],
61 },
62});
63```
64
65Options set on the resource (`srcDir`, `distDir`, `basePath`) merge
66over the file, per key. The one key Alchemy owns is
67`unstable_adapter` — the Node deploy target sets it. Setting it
68fails the build with an actionable error.
69
70:::note
71Vite configuration must live in `waku.config.ts`'s `vite` field —
72a standalone `vite.config.ts` is not loaded, same as Waku's own CLI.
73:::
74
75## Declare the Website
76
77Declare the site as a module-level const (rather than inline in the
78Stack):
79
80```typescript
81// alchemy.run.ts
82import * as Hetzner from "alchemy/Hetzner";
83
84export const Website = Hetzner.Website.Waku("Website");
85```
86
87Pass `server` to host the site on an existing
88[`Hetzner.Server`](/hetzner/compute/servers) — several sites and
89APIs can share one box. `rootDir` defaults to `"."`.
90
91## Add it to the Stack
92
93Yield the site from your Stack and return its URL:
94
95```typescript
96// alchemy.run.ts
97import * as Alchemy from "alchemy";
98import * as Effect from "effect/Effect";
99
100export default Alchemy.Stack(
101 "MyWakuSite",
102 {
103 providers: Hetzner.providers(),
104 state: Alchemy.localState(),
105 },
106 Effect.gen(function* () {
107 const site = yield* Website;
108 return { url: site.url };
109 }),
110);
111```
112
113Pages rendered as `"static"` are generated at build time and baked
114into the unit; dynamic pages and API routes run on the Server.
115Prerendered pages are served at their extensionless URLs (`/about`,
116not `/about/`).
117
118The live `url` is `http://{ipv4}:3000`. `site.server` and
119`site.service` are the Hetzner resources underneath — `undefined`
120during `alchemy dev`.
121
122See the [Waku API reference](/providers/hetzner/website/waku) for
123every prop and attribute.
124
125## Add environment variables
126
127Process environment is a top-level `env` map — plain strings, or
128`Redacted` values:
129
130```diff lang="typescript"
131// alchemy.run.ts
132export const Website = Hetzner.Website.Waku("Website", {
133+ env: {
134+ GREETING: "Hello from alchemy",
135+ API_BASE: "https://api.example.com",
136+ },
137});
138```
139
140The values are copied onto `process.env` before the build and the
141dev server, and onto the systemd unit at deploy time, so server
142code reads the same keys in both modes. Client inlining is whatever
143Vite does (`VITE_` prefixes).
144
145## Read the environment in server code
146
147The RSC server is a plain Node process. Server components and API
148routes read `process.env` (Waku's `getEnv` reads the same values):
149
150```typescript
151// src/pages/index.tsx
152export default async function HomePage() {
153 const greeting = process.env.GREETING ?? "hello";
154 return <h1>{greeting}</h1>;
155}
156
157export const getConfig = async () => ({ render: "dynamic" }) as const;
158```
159
160## Local dev
161
162```sh
163bun alchemy dev
164```
165
166`alchemy dev` runs Waku's own dev server (native HMR) instead of
167deploying; `site.url` is the local address and no Hetzner resources
168are created. Wrap the site in `Alchemy.remote()` to deploy the live
169Server and Service even during dev:
170
171```typescript
172export const Website = Hetzner.Website.Waku("Website").pipe(
173 Alchemy.remote(),
174);
175```
176
177## Custom domain
178
179```typescript
180const zone = yield* Hetzner.Zone("dns", { name: "example.com" });
181const site = yield* Hetzner.Website.Waku("Web", {
182 domain: "app.example.com",
183 zone,
184});
185```
186
187`domain` requires an existing [`Hetzner.Zone`](/hetzner/networking/dns)
188— the Website does not create one. It adds an A record pointing at
189the Server's public IPv4. `url` becomes `http://app.example.com:3000`.
190There is no TLS on the Service.
191
192## Where next
193
194- [Waku API reference](/providers/hetzner/website/waku)
195- [Servers](/hetzner/compute/servers) and
196 [Services](/hetzner/compute/services)
197- [DNS](/hetzner/networking/dns)