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