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