Part 2: Deploy a Service
In Part 1 you deployed a Project. Now you
will deploy a Railway.Service: an Effect program running in a
Railway container. Alchemy bundles it, generates a Dockerfile, and
uploads the context. Railway builds the image.
Move the Project to a module
Section titled “Move the Project to a module”So far the Project lives inside the Stack’s generator. To let other
files reference it, declare it at module scope instead — a
resource declared this way is an Effect you can import and yield*
anywhere. Create src/project.ts:
import * as Railway from "alchemy/Railway";
export const Site = Railway.Project("Site");The logical id is still Site, so Alchemy recognizes it as the same
Project you deployed in Part 1 — moving declarations between files
doesn’t recreate anything.
Update the Stack
Section titled “Update the Stack”Import the Project and yield it from the Stack:
import * as Alchemy from "alchemy";import * as Railway from "alchemy/Railway";import * as Effect from "effect/Effect";import { Site } from "./src/project.ts";
export default Alchemy.Stack( "MyApp", { providers: Railway.providers(), state: Alchemy.localState(), }, Effect.gen(function* () { const site = yield* Railway.Project("Site"); const site = yield* Site;
return { name: site.name, projectId: site.projectId, url: site.url, }; }),);Create the Service file
Section titled “Create the Service file”A Service in Alchemy is a class — it has both an infrastructure
definition and a runtime implementation expressed as an Effect.
Create src/api.ts with the smallest possible declaration:
import * as Railway from "alchemy/Railway";import * as Effect from "effect/Effect";import { Site } from "./project.ts";
export default class Api extends Railway.Service<Api>()( "Api", { project: Site, main: import.meta.url, }, Effect.gen(function* () { return {}; }),) {}The <Api> type argument plus the empty () is a one-time bit of
ceremony — it lets TypeScript reason about Api as a typed handle.
project: Site says which Project this Service runs in, and
main: import.meta.url tells Alchemy this same file is the bundle
entrypoint.
Serve HTTP with fetch
Section titled “Serve HTTP with fetch”Add a fetch field — Alchemy treats anything returned from the
Effect.gen block as the runtime API, and fetch specifically is
wired to an HTTP server listening on the Service’s port:
import * as Railway from "alchemy/Railway";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";import { Site } from "./project.ts";
export default class Api extends Railway.Service<Api>()( "Api", { project: Site, main: import.meta.url, }, Effect.gen(function* () { return {}; return { fetch: Effect.succeed(HttpServerResponse.text("Hello from Railway!")), }; }),) {}HttpServerResponse is the same effect/unstable/http API used on
every other Alchemy runtime — the handler you write here would run
unchanged on Cloudflare Workers or AWS Lambda.
Pin a region
Section titled “Pin a region”Railway Services live in a region. Omit it and Railway picks a default. Set it explicitly so the Volume you add in Part 3 can match:
export default class Api extends Railway.Service<Api>()( "Api", { project: Site, main: import.meta.url }, { project: Site, main: import.meta.url, region: "us-west2" },Updating region later is in place.
Set the port
Section titled “Set the port”Give the Service an explicit port so Railway’s edge knows where to forward:
export default class Api extends Railway.Service<Api>()( "Api", { project: Site, main: import.meta.url, region: "us-west2" }, { project: Site, main: import.meta.url, region: "us-west2", port: 3000 },The port is written to the process environment as PORT and used as
the generated domain’s targetPort. Alchemy publishes HTTPS on
Railway’s edge in front of it.
Add a health route
Section titled “Add a health route”Add /health so you can probe the process:
import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";// ... return { fetch: Effect.succeed(HttpServerResponse.text("Hello from Railway!")), fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; const url = new URL(request.url, "http://service"); if (url.pathname === "/health") { return HttpServerResponse.json({ ok: true }); } return HttpServerResponse.text("Hello from Railway!"); }), };Wire the Service into the Stack
Section titled “Wire the Service into the Stack”The Api class is just a typed identifier — yielding it inside the
Stack’s Effect is what registers the resource and starts the deploy:
import * as Alchemy from "alchemy";import * as Railway from "alchemy/Railway";import * as Effect from "effect/Effect";import Api from "./src/api.ts";import { Site } from "./src/project.ts";
export default Alchemy.Stack( "MyApp", { providers: Railway.providers(), state: Alchemy.localState(), }, Effect.gen(function* () { const site = yield* Site; const api = yield* Api;
return { name: site.name, projectId: site.projectId, url: site.url, url: api.url, }; }),);Yielding Api returns the resolved Service outputs — the service
id, region, and the public url.
Deploy
Section titled “Deploy”bun alchemy deploynpm run alchemy deploypnpm alchemy deployyarn alchemy deployPlan: 1 to create + Api (Railway.Service) • Site (Railway.Project) Proceed? ◉ Yes ○ No ✓ Api (Railway.Service) created { name: "myapp-site-dev-a1b2c3d4", projectId: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", url: "https://myapp-api-dev-a1b2c3d4.up.railway.app", }
A lot just happened:
src/api.tswas bundled with Rolldown into a single ESM file.- Alchemy generated a Dockerfile (
FROM oven/bun:1) and uploaded the context. Railway built the image. - It created the Service, attached a
*.up.railway.appdomain, and pointed Railway’s edge at port 3000.
Try it out
Section titled “Try it out”curl https://myapp-api-dev-a1b2c3d4.up.railway.app# → Hello from Railway!Ship a change
Section titled “Ship a change”Edit the greeting in src/api.ts and deploy again:
Plan: 1 to update ~ Api (Railway.Service) Proceed? ◉ Yes ○ No ✓ Api (Railway.Service) updated
Alchemy hashes the bundle: if your code didn’t change, the Service is a no-op; if it did, a new image is built, pushed, and the Service updated in place.
You now have:
- An HTTP Service running as a Railway container in
us-west2 - A public
https://{name}.up.railway.appURL - A code-hash-based update loop — edit, deploy, new image
In Part 3, you’ll attach a Volume and persist data across deploys.