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