1---
2title: Static sites
3description: "Deploy a static site to Hetzner with Hetzner.Website.StaticSite — a build command, a systemd unit on a Server, and http://{ipv4}:3000 (or your hostname) on deploy."
4sidebar:
5 order: 4
6---
7
8`Hetzner.Website.StaticSite` runs a build command, packs the output
9directory into the unit archive, and deploys a tiny static-file
10server (`GET` assets, `/health`, optional SPA / 404-page) as a
11[Service](/hetzner/compute/services) on a
12[Server](/hetzner/compute/servers). Use it when the site has its own
13build step — Hugo, Zola, Eleventy, or any custom pipeline.
14
15For Vite-based projects, prefer
16[`Hetzner.Website.Vite`](/hetzner/frontend/vite).
17
18## Declare the Website
19
20`command` and `outdir` are required. Declare the site as a
21module-level const:
22
23```typescript
24// alchemy.run.ts
25import * as Hetzner from "alchemy/Hetzner";
26
27export const Website = Hetzner.Website.StaticSite("Website", {
28 command: "hugo --minify",
29 outdir: "public",
30});
31```
32
33The build is a [`Command.Build`](/providers/command/build) —
34memoized, so unchanged inputs skip the rebuild
35([details](/command/memoization)). By default every non-gitignored
36file in `cwd` (plus the nearest lockfile) is hashed. Narrow it with
37`memo: { include: [...] }`, or set `memo: false` to rebuild on every
38deploy.
39
40## Add it to the Stack
41
42Yield the site from your Stack and return its URL:
43
44```typescript
45// alchemy.run.ts
46import * as Alchemy from "alchemy";
47import * as Effect from "effect/Effect";
48
49export default Alchemy.Stack(
50 "MyStaticSite",
51 {
52 providers: Hetzner.providers(),
53 state: Alchemy.localState(),
54 },
55 Effect.gen(function* () {
56 const site = yield* Website;
57 return { url: site.url };
58 }),
59);
60```
61
62One `StaticSite` expands into a Server (default `cpx12` /
63`ubuntu-24.04` / `fsn1` if you omit `server`) and a systemd unit on
64port 3000. Several sites or APIs can share one Server. Live `url` is
65`http://{ipv4}:3000` (or `http://{domain}:3000` when you pass
66`domain`). There is no TLS on the Service. `server` and `service`
67are undefined under `alchemy dev`.
68
69Pass `server` to colocate the unit with other Services:
70
71```typescript
72export const Box = Hetzner.Server("Box", {
73 serverType: "cpx12",
74 image: "ubuntu-24.04",
75 location: "fsn1",
76});
77
78export const Website = Hetzner.Website.StaticSite("Website", {
79 command: "hugo --minify",
80 outdir: "public",
81 server: Box,
82});
83```
84
85## Building from a subdirectory
86
87`cwd` is the working directory for `command`. `outdir` is relative
88to `cwd` (default: process cwd):
89
90```typescript
91export const Website = Hetzner.Website.StaticSite("Web", {
92 cwd: "apps/web",
93 command: "npm run build",
94 outdir: "dist",
95});
96```
97
98## SPAs and 404 pages
99
100A miss — a request that matches no output file — can be answered two
101ways, and they are mutually exclusive (passing both fails the
102deploy):
103
104```typescript
105// Single-page app: misses serve index.html with a 200 so the
106// client-side router takes over.
107export const App = Hetzner.Website.StaticSite("App", {
108 command: "npm run build",
109 outdir: "dist",
110 spa: true,
111});
112
113// Static site: misses return a real 404 status with your error page.
114export const Docs = Hetzner.Website.StaticSite("Docs", {
115 command: "hugo --minify",
116 outdir: "public",
117 errorPage: "404.html",
118});
119```
120
121## Add environment variables
122
123Top-level `env` is copied onto `process.env` for the build and onto
124the systemd unit at deploy. Wrap secrets with `Redacted`. It is not
125Worker bindings and not AWS `server.environment`:
126
127```diff lang="typescript"
128export const Website = Hetzner.Website.StaticSite("Website", {
129 command: "hugo --minify",
130 outdir: "public",
131+ env: {
132+ HUGO_ENV: "production",
133+ },
134});
135```
136
137Generators that inline at build time pick these up. The hosted
138process is the generated static-file server — there is no framework
139SSR runtime.
140
141## Local development
142
143During `alchemy dev`, `dev.command` replaces the build with that
144process as a long-lived child. No Server or Service is created, and
145`site.url` is the local address detected from stdout (`dev.url`
146pins it when detection fails). `dev` also accepts `cwd` and `env`
147overrides:
148
149```typescript
150export const Website = Hetzner.Website.StaticSite("Website", {
151 command: "hugo --minify",
152 outdir: "public",
153 dev: { command: "hugo server" },
154});
155```
156
157Without `dev.command`, the site still builds and a local static
158server serves `outdir` — still no cloud resources. Wrap the site in
159`Alchemy.remote()` to deploy the live path even during dev:
160
161```typescript
162export const Website = Hetzner.Website.StaticSite("Website", {
163 command: "hugo --minify",
164 outdir: "public",
165}).pipe(Alchemy.remote());
166```
167
168## Custom domain
169
170`domain` requires `zone` — a
171[`Hetzner.Zone`](/hetzner/networking/dns) you pass in. The site
172does not create one. Alchemy creates an A `RecordSet` pointing at
173the Server's public IPv4, and `url` becomes `http://{domain}:3000`.
174The Service still has no TLS:
175
176```typescript
177export const Dns = Hetzner.Zone("Dns", {
178 name: "example.com",
179});
180
181export const Website = Hetzner.Website.StaticSite("Website", {
182 command: "hugo --minify",
183 outdir: "public",
184 domain: "blog.example.com",
185 zone: Dns,
186});
187```
188
189Passing `domain` without `zone` fails the deploy.
190
191## When to use a framework resource instead
192
193`StaticSite` is the general fallback for any shell command that
194produces a directory of files. Frameworks with dedicated resources
195have a better path: [Astro](/hetzner/frontend/astro),
196[Foldkit](/hetzner/frontend/foldkit),
197[Next.js](/hetzner/frontend/nextjs), [Nuxt](/hetzner/frontend/nuxt),
198[Octane](/hetzner/frontend/octane),
199[React Router](/hetzner/frontend/react-router),
200[SolidStart](/hetzner/frontend/solidstart),
201[SvelteKit](/hetzner/frontend/sveltekit),
202[TanStack Start](/hetzner/frontend/tanstack-start),
203[Vite](/hetzner/frontend/vite), [Vocs](/hetzner/frontend/vocs), and
204[Waku](/hetzner/frontend/waku). Those resources run the framework's
205own programmatic build and skip the `command` / `outdir` contract.
206Reach for `StaticSite` when there is no dedicated resource — Zola,
207Hugo, or any other generator.
208
209## Where next
210
211- [Websites on Hetzner](/hetzner/frontend/websites) — the full
212 websites surface, including Node SSR frameworks
213- [StaticSite reference](/providers/hetzner/website/staticsite) —
214 every prop and attribute
215- [Hetzner on Alchemy](/hetzner) — the Hetzner provider hub