1---
2title: Astro
3description: Deploy an Astro app to Railway with Railway.Website.Astro — Node SSR on a Service container, static output as a file server, and Astro's own dev server under alchemy dev.
4sidebar:
5 order: 11
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12`Railway.Website.Astro` deploys an [Astro](https://astro.build)
13project to Railway as a container. Server-rendered pages run on a
14[`Railway.Service`](/railway/compute/services) (port 3000); static
15files are served first, then the Astro handler. Omit `project` and
16Alchemy creates a [`Railway.Project`](/railway/compute/projects).
17Your `astro.config.*` loads natively — don't declare an `adapter`.
18
19## Install
20
21The build integration is not bundled with alchemy. Install
22`@alchemy.run/frontend-frameworks`; the resource loads its `/astro`
23and `/astro/node` exports from your project at deploy time. It is
24only used at build time, so a dev dependency is enough:
25
26<Tabs syncKey="pkgManager">
27 <TabItem label="bun" icon="bun">
28 <Code code={`bun add -d ${devDeps}`} lang="sh" />
29 </TabItem>
30 <TabItem label="npm" icon="npm">
31 <Code code={`npm install -D ${devDeps}`} lang="sh" />
32 </TabItem>
33 <TabItem label="pnpm" icon="pnpm">
34 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
35 </TabItem>
36 <TabItem label="yarn" icon="seti:yarn">
37 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
38 </TabItem>
39</Tabs>
40
41## Configure Astro
42
43Your `astro.config.*` file loads natively — integrations, Vite
44plugins, and any other non-serializable options work exactly as they
45do outside Alchemy. Alchemy merges a programmatic config *over* it:
46the Node adapter is injected for you (don't declare an `adapter` in
47your config — that fails the build with an actionable error), and
48options passed via the `astro` prop override the file's — see
49[Astro configuration](#astro-configuration) below for the details.
50
51## Declare the Website
52
53Declare the site as a module-level const (rather than inline in the
54Stack):
55
56```typescript
57// alchemy.run.ts
58import * as Railway from "alchemy/Railway";
59
60export const Website = Railway.Website.Astro("Website");
61```
62
63Pass `rootDir` if `package.json` is not at `.`. Pass `project` to
64put the Service in a Project you already declared; otherwise the
65site creates `Railway.Project("Project")` under its namespace.
66
67## Add it to the Stack
68
69Yield the site from your Stack and return its URL:
70
71```typescript
72// alchemy.run.ts
73import * as Alchemy from "alchemy";
74import * as Effect from "effect/Effect";
75
76export default Alchemy.Stack(
77 "MyAstroSite",
78 {
79 providers: Railway.providers(),
80 state: Alchemy.localState(),
81 },
82 Effect.gen(function* () {
83 const site = yield* Website;
84 return { url: site.url };
85 }),
86);
87```
88
89Pages are server-rendered by default. Pages that
90`export const prerender = true` are prerendered at build time and
91served as static files.
92
93On deploy `site.url` is the generated `*.up.railway.app` hostname
94(or `https://{domain}` when you set `domain`). `site.service` and
95`site.project` are undefined under `alchemy dev`.
96
97## Add environment variables
98
99Process environment is the top-level `env` prop — plain strings, or
100`Redacted` values. They are copied onto `process.env` before build
101and `alchemy dev`, and onto the Service at deploy:
102
103```diff lang="typescript"
104// alchemy.run.ts
105export const Website = Railway.Website.Astro("Website", {
106+ env: {
107+ GREETING: "Hello from Alchemy!",
108+ API_BASE: "https://api.example.com",
109+ },
110});
111```
112
113This is the hosted Node process, not Worker bindings.
114
115## Read the environment in server code
116
117The Astro server runs in Node in the container, so server-rendered
118pages and endpoints read the environment from `process.env`:
119
120```astro
121---
122// src/pages/index.astro
123const greeting = process.env.GREETING ?? "hello";
124---
125
126<h1>{greeting}</h1>
127```
128
129Astro inlines `PUBLIC_*` keys at build time. Because `env` is
130applied before the build, those keys behave the same as in a local
131`astro build`.
132
133## Fully static sites
134
135With `astro: { output: "static" }` every page is prerendered at
136build time and the deploy is assets-only — no Astro handler, just a
137static-file server in the image:
138
139```typescript
140export const Website = Railway.Website.Astro("Website", {
141 astro: { output: "static" },
142 assets: { notFoundHandling: "404-page" },
143});
144```
145
146`assets.notFoundHandling` applies only to `output: "static"`.
147`"404-page"` serves the built `404.html` for unmatched routes;
148`"single-page-application"` answers misses with the index page
149(200) instead.
150
151## Astro configuration
152
153Your `astro.config.*` is the primary place to configure Astro —
154integrations, Vite plugins, and every other option (serializable or
155not) work as usual:
156
157```typescript
158// astro.config.ts
159import { defineConfig } from "astro/config";
160import react from "@astrojs/react";
161import tailwindcss from "@tailwindcss/vite";
162
163export default defineConfig({
164 site: "https://blog.example.com",
165 integrations: [react()],
166 vite: {
167 plugins: [tailwindcss()],
168 },
169});
170```
171
172The `astro` prop on the resource exposes common serializable options
173for deploy-specific overrides; Astro merges them *over* the config
174file (scalars override, arrays like `integrations` and
175`vite.plugins` concatenate after the file's):
176
177```typescript
178// alchemy.run.ts
179export const Website = Railway.Website.Astro("Website", {
180 astro: {
181 site: "https://preview.example.com",
182 },
183});
184```
185
186Two options are managed for you regardless of the config file:
187
188- `adapter` — Alchemy injects the Node adapter. Declaring an
189 adapter in `astro.config.*` fails the build with an actionable
190 error.
191- `output` — Alchemy defaults it to `"server"` (superseding a
192 file-level `output`), so pages render on demand in the container.
193 Opt into a fully prerendered site with `astro: { output: "static" }`
194 on the resource.
195
196`site`, `base`, `srcDir`, `publicDir`, `outDir`, and
197`trailingSlash` are the other serializable keys on `astro`.
198
199## Local dev
200
201`alchemy dev` runs Astro's own dev server (native HMR) instead of
202deploying; `site.url` is the local address and no Railway resources
203are created. Wrap the site in
204`Alchemy.remote()` to deploy the real Project and Service even
205during dev.
206
207## Custom domain
208
209`domain` is a hostname string. The site attaches a
210[`Railway.CustomDomain`](/railway/networking#custom-domains)
211(`targetPort` 3000) and `url` becomes `https://{domain}` instead of
212the generated `*.up.railway.app`:
213
214```typescript
215export const Website = Railway.Website.Astro("Website", {
216 domain: "app.example.com",
217});
218```
219
220Railway issues a Let's Encrypt certificate once DNS is verified.
221
222## Where next
223
224- [`Railway.Website.Astro` reference](/providers/railway/website/astro)
225 — every prop and attribute.
226- [Services](/railway/compute/services) and
227 [Projects](/railway/compute/projects) — the Service and Project
228 the site creates.
229- [Custom domains](/railway/networking#custom-domains) — hostnames
230 on a Service.
231- [Setup](/railway/setup) — workspace, API token, and alchemy
232 login.