Astro
Railway.Website.Astro deploys an Astro
project to Railway as a container. Server-rendered pages run on a
Railway.Service (port 3000); static
files are served first, then the Astro handler. Omit project and
Alchemy creates a Railway.Project.
Your astro.config.* loads natively — don’t declare an adapter.
Install
Section titled “Install”The build integration is not bundled with alchemy. Install
@alchemy.run/frontend-frameworks; the resource loads its /astro
and /astro/node exports from your project at deploy time. It is
only used at build time, so a dev dependency is enough:
bun add -d @alchemy.run/frontend-frameworksnpm install -D @alchemy.run/frontend-frameworkspnpm add -D @alchemy.run/frontend-frameworksyarn add -D @alchemy.run/frontend-frameworksConfigure Astro
Section titled “Configure Astro”Your astro.config.* file loads natively — integrations, Vite
plugins, and any other non-serializable options work exactly as they
do outside Alchemy. Alchemy merges a programmatic config over it:
the Node adapter is injected for you (don’t declare an adapter in
your config — that fails the build with an actionable error), and
options passed via the astro prop override the file’s — see
Astro configuration below for the details.
Declare the Website
Section titled “Declare the Website”Declare the site as a module-level const (rather than inline in the Stack):
import * as Railway from "alchemy/Railway";
export const Website = Railway.Website.Astro("Website");Pass rootDir if package.json is not at .. Pass project to
put the Service in a Project you already declared; otherwise the
site creates Railway.Project("Project") under its namespace.
Add it to the Stack
Section titled “Add it to the Stack”Yield the site from your Stack and return its URL:
import * as Alchemy from "alchemy";import * as Effect from "effect/Effect";
export default Alchemy.Stack( "MyAstroSite", { providers: Railway.providers(), state: Alchemy.localState(), }, Effect.gen(function* () { const site = yield* Website; return { url: site.url }; }),);Pages are server-rendered by default. Pages that
export const prerender = true are prerendered at build time and
served as static files.
On deploy site.url is the generated *.up.railway.app hostname
(or https://{domain} when you set domain). site.service and
site.project are undefined under alchemy dev.
Add environment variables
Section titled “Add environment variables”Process environment is the top-level env prop — plain strings, or
Redacted values. They are copied onto process.env before build
and alchemy dev, and onto the Service at deploy:
export const Website = Railway.Website.Astro("Website", { env: { GREETING: "Hello from Alchemy!", API_BASE: "https://api.example.com", },});This is the hosted Node process, not Worker bindings.
Read the environment in server code
Section titled “Read the environment in server code”The Astro server runs in Node in the container, so server-rendered
pages and endpoints read the environment from process.env:
---const greeting = process.env.GREETING ?? "hello";---
<h1>{greeting}</h1>Astro inlines PUBLIC_* keys at build time. Because env is
applied before the build, those keys behave the same as in a local
astro build.
Fully static sites
Section titled “Fully static sites”With astro: { output: "static" } every page is prerendered at
build time and the deploy is assets-only — no Astro handler, just a
static-file server in the image:
export const Website = Railway.Website.Astro("Website", { astro: { output: "static" }, assets: { notFoundHandling: "404-page" },});assets.notFoundHandling applies only to output: "static".
"404-page" serves the built 404.html for unmatched routes;
"single-page-application" answers misses with the index page
(200) instead.
Astro configuration
Section titled “Astro configuration”Your astro.config.* is the primary place to configure Astro —
integrations, Vite plugins, and every other option (serializable or
not) work as usual:
import { defineConfig } from "astro/config";import react from "@astrojs/react";import tailwindcss from "@tailwindcss/vite";
export default defineConfig({ site: "https://blog.example.com", integrations: [react()], vite: { plugins: [tailwindcss()], },});The astro prop on the resource exposes common serializable options
for deploy-specific overrides; Astro merges them over the config
file (scalars override, arrays like integrations and
vite.plugins concatenate after the file’s):
export const Website = Railway.Website.Astro("Website", { astro: { site: "https://preview.example.com", },});Two options are managed for you regardless of the config file:
adapter— Alchemy injects the Node adapter. Declaring an adapter inastro.config.*fails the build with an actionable error.output— Alchemy defaults it to"server"(superseding a file-leveloutput), so pages render on demand in the container. Opt into a fully prerendered site withastro: { output: "static" }on the resource.
site, base, srcDir, publicDir, outDir, and
trailingSlash are the other serializable keys on astro.
Local dev
Section titled “Local dev”alchemy dev runs Astro’s own dev server (native HMR) instead of
deploying; site.url is the local address and no Railway resources
are created. Wrap the site in
Alchemy.remote() to deploy the real Project and Service even
during dev.
Custom domain
Section titled “Custom domain”domain is a hostname string. The site attaches a
Railway.CustomDomain
(targetPort 3000) and url becomes https://{domain} instead of
the generated *.up.railway.app:
export const Website = Railway.Website.Astro("Website", { domain: "app.example.com",});Railway issues a Let’s Encrypt certificate once DNS is verified.
Where next
Section titled “Where next”Railway.Website.Astroreference — every prop and attribute.- Services and Projects — the Service and Project the site creates.
- Custom domains — hostnames on a Service.
- Setup — workspace, API token, and alchemy login.