1---
2title: Astro
3description: Deploy an Astro app to AWS with AWS.Website.Astro — SSR on a streaming Lambda Function URL, static output on S3 + CloudFront, 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`AWS.Website.Astro` deploys an [Astro](https://astro.build) project
13to AWS. It runs Astro's programmatic build with a wrangler-free AWS
14Lambda adapter: server-rendered pages stream from a Lambda Function
15URL, prerendered pages and client assets are served from a private S3
16bucket through CloudFront, routed at the edge. Your `astro.config.*`
17loads natively — there is no adapter to install into it and no
18CloudFormation to write.
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/aws` exports from your project at deploy time. It is only
25used 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 AWS Lambda adapter is injected for you (don't declare an
48`adapter` in your config — that fails the build with an actionable
49error), and options passed via the `astro` prop override the file's —
50see [Astro configuration](#astro-configuration) below for the
51details.
52
53## Declare the Website
54
55Declare the site as a module-level const (rather than inline in the
56Stack):
57
58```typescript
59// alchemy.run.ts
60import * as AWS from "alchemy/AWS";
61
62export const Website = AWS.Website.Astro("Website");
63```
64
65## Add it to the Stack
66
67Yield the site from your Stack and return its URL:
68
69```typescript
70// alchemy.run.ts
71import * as Alchemy from "alchemy";
72import * as Effect from "effect/Effect";
73
74export default Alchemy.Stack(
75 "MyAstroSite",
76 {
77 providers: AWS.providers(),
78 state: AWS.state(),
79 },
80 Effect.gen(function* () {
81 const site = yield* Website;
82 return { url: site.url };
83 }),
84);
85```
86
87Pages are server-rendered by default. Pages that
88`export const prerender = true` are uploaded to S3 and served from
89the edge without invoking the server.
90
91See
92[examples/aws-website-astro](https://github.com/alchemy-run/alchemy/tree/main/examples/aws-website-astro)
93for the checked-in example.
94
95## Add environment variables
96
97The server function's environment is configured under `env` — plain
98values, or outputs from other resources in the Stack:
99
100```diff lang="typescript"
101// alchemy.run.ts
102export const Website = AWS.Website.Astro("Website", {
103+ env: {
104+ GREETING: "Hello from Alchemy!",
105+ API_BASE: api.url,
106+ },
107});
108```
109
110The values are set on the Lambda at deploy time and injected into the
111dev server's process environment under `alchemy dev`, so server code
112reads the same values in both modes.
113
114## Read the environment in server code
115
116The Astro server runs in a plain Node Lambda, so server-rendered
117pages and endpoints read the environment from `process.env`:
118
119```astro
120---
121// src/pages/index.astro
122const greeting = process.env.GREETING ?? "hello";
123---
124
125<h1>{greeting}</h1>
126```
127
128## Fully static sites
129
130With `astro: { output: "static" }` every page is prerendered at
131build time and the deploy is assets-only — no Lambda is created, and
132the S3 + CloudFront asset layer answers every request:
133
134```typescript
135export const Website = AWS.Website.Astro("Website", {
136 astro: { output: "static" },
137 errorPage: "404.html",
138});
139```
140
141`errorPage` serves the built `404.html` for unmatched routes; `spa`
142answers misses with the index page (200) instead.
143
144## Astro configuration
145
146Your `astro.config.*` is the primary place to configure Astro —
147integrations, Vite plugins, and every other option (serializable or
148not) work as usual:
149
150```typescript
151// astro.config.ts
152import { defineConfig } from "astro/config";
153import react from "@astrojs/react";
154import tailwindcss from "@tailwindcss/vite";
155
156export default defineConfig({
157 site: "https://blog.example.com",
158 integrations: [react()],
159 vite: {
160 plugins: [tailwindcss()],
161 },
162});
163```
164
165A config file can't consume Alchemy `Output`s or vary by stage. For
166those values the `astro` prop is a deploy-time override bag — a
167serializable subset (`site`, `base`, `output`, `srcDir`, `publicDir`,
168`outDir`, `trailingSlash`) merged *over* the config file, so a value
169set here wins:
170
171```typescript
172// alchemy.run.ts
173import { Stack } from "alchemy/Stack";
174
175Effect.gen(function* () {
176 const { stage } = yield* Stack;
177
178 const site = yield* AWS.Website.Astro("Website", {
179 astro: {
180 site:
181 stage === "prod"
182 ? "https://blog.example.com"
183 : `https://${stage}.example.com`,
184 },
185 });
186});
187```
188
189Use `config` to load an alternate config file (relative to
190`rootDir`) instead of astro's own `astro.config.*` discovery:
191
192```typescript
193export const Website = AWS.Website.Astro("Website", {
194 config: "astro.deploy.config.ts",
195});
196```
197
198Two options are managed for you regardless of the config file:
199
200- `adapter` — Alchemy injects its wrangler-free AWS Lambda adapter.
201 Declaring an adapter in `astro.config.*` fails the build with an
202 actionable error.
203- `output` — Alchemy defaults it to `"server"` (superseding a
204 file-level `output`), so pages render on demand in the Lambda.
205 Opt into a fully prerendered site with `astro: { output: "static" }`
206 on the resource.
207
208## Local dev
209
210`alchemy dev` runs Astro's own dev server (native HMR) instead of
211deploying; `site.url` is the local address and no AWS resources are
212created. Wrap the site in `Alchemy.remote()` to deploy the real
213infrastructure even during dev.
214
215## Custom domain
216
217```typescript
218const site = yield* AWS.Website.Astro("Web", {
219 domain: { name: "app.example.com" },
220});
221```
222
223The hosted zone is inferred from the hostname. Pass `hostedZoneId` to
224pin it when several zones could match.
225
226:::note
227Astro's default image service uses `sharp`. To use Astro image
228optimization on AWS, add `sharp` as a project dependency so it ships
229with the server bundle.
230:::