Skip to content

Static sites

Fly.Website.StaticSite runs a build command, content-hashes the output directory, and deploys a Service that serves those files (plus GET /health) from a tiny Node static-file server. Use it when the site has its own build step — Hugo, Zola, Eleventy, or any custom pipeline.

For Vite-based projects, prefer Fly.Website.Vite.

build.command and build.output are required. Declare the site as a module-level const:

alchemy.run.ts
import * as Fly from "alchemy/Fly";
export const Website = Fly.Website.StaticSite("Website", {
build: { command: "hugo --minify", output: "public" },
});

The build is a Command.Build — memoized, so unchanged inputs skip the rebuild (details). By default every non-gitignored file in path (plus the nearest lockfile) is hashed. Narrow it with memo: { include: [...] }, or set memo: false to rebuild on every deploy.

Yield the site from your Stack and return its URL:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
export default Alchemy.Stack(
"MyStaticSite",
{
providers: Fly.providers(),
state: Alchemy.localState(),
},
Effect.gen(function* () {
const site = yield* Website;
return { url: site.url };
}),
);

One StaticSite expands into a Fly App (created if you omit app), a Service running Node on a Machine (port 3000), and a shared IPv4 so https://{app}.fly.dev answers. Built files are packed at /app/dist. site.url is that fly.dev URL (or https://{domain} when you pass domain). app, service, ip, and certificate are undefined during alchemy dev.

Pass app to put the site on an App you already declared:

export const Site = Fly.App("Site");
export const Website = Fly.Website.StaticSite("Website", {
build: { command: "hugo --minify", output: "public" },
app: Site,
});

path is the working directory for build.command. build.output is relative to path (default: process cwd):

export const Website = Fly.Website.StaticSite("Web", {
path: "apps/web",
build: { command: "npm run build", output: "dist" },
});

A miss — a request that matches no output file — can be answered two ways, and they are mutually exclusive:

// Single-page app: misses serve index.html with a 200 so the
// client-side router takes over.
export const App = Fly.Website.StaticSite("App", {
build: { command: "npm run build", output: "dist" },
assets: { notFoundHandling: "single-page-application" },
});
// Static site: misses return a real 404 status with your error page.
export const Docs = Fly.Website.StaticSite("Docs", {
build: { command: "hugo --minify", output: "public" },
assets: { notFoundHandling: "404-page" },
});

Top-level env is copied onto process.env for the build and onto the Machine at deploy. Wrap secrets with Redacted. It is not Worker bindings and not AWS server.environment:

export const Website = Fly.Website.StaticSite("Website", {
build: { command: "hugo --minify", output: "public" },
env: {
HUGO_ENV: "production",
},
});

Generators that inline at build time pick these up. The hosted process is the generated static-file server — there is no framework SSR runtime.

During alchemy dev, dev.command replaces the build with that process as a long-lived child. No Fly App, Service, IP, or certificate is created, and site.url is the local address detected from stdout (dev.url pins it when detection fails). dev also accepts cwd and env overrides:

export const Website = Fly.Website.StaticSite("Website", {
build: { command: "hugo --minify", output: "public" },
dev: { command: "hugo server" },
});

Without dev.command, the site still builds and a local static server serves outdir — still no cloud resources. Wrap the site in Alchemy.remote() to deploy the live path even during dev:

export const Website = Fly.Website.StaticSite("Website", {
build: { command: "hugo --minify", output: "public" },
}).pipe(Alchemy.remote());

domain is a hostname string. Alchemy requests ACME (Fly.Certificate) on the App and url becomes https://{domain}. Point DNS at the App yourself — v1 does not create records:

export const Website = Fly.Website.StaticSite("Website", {
build: { command: "hugo --minify", output: "public" },
domain: "blog.example.com",
});

See IPs & certificates for A/AAAA records and the ACME challenge.

StaticSite is the general fallback for any shell command that produces a directory of files. Frameworks with dedicated resources have a better path: Astro, Foldkit, Next.js, Nuxt, Octane, React Router, SolidStart, SvelteKit, TanStack Start, Vite, Vocs, and Waku. Those resources run the framework’s own programmatic build and skip the build.command / build.output contract. Reach for StaticSite when there is no dedicated resource — Zola, Hugo, or any other generator.