1---
2title: "Part 2: Deploy a Service"
3description: Bundle an Effect HTTP server, deploy it to Railway as a Service, and serve requests on up.railway.app.
4sidebar:
5 order: 2
6---
7
8import Terminal from "../../../../components/Terminal.astro";
9import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
10
11In [Part 1](/railway/tutorial/part-1) you deployed a Project. Now you
12will deploy a `Railway.Service`: an Effect program running in a
13Railway container. Alchemy bundles it, generates a Dockerfile, and
14uploads the context. Railway builds the image.
15
16:::note[Image Services]
17Pass `image: "hashicorp/http-echo"` to skip the build and pull a
18public tag instead — see [Services](/railway/compute/services).
19:::
20
21## Move the Project to a module
22
23So far the Project lives inside the Stack's generator. To let other
24files reference it, declare it at **module scope** instead — a
25resource declared this way is an Effect you can import and `yield*`
26anywhere. Create `src/project.ts`:
27
28```typescript
29// src/project.ts
30import * as Railway from "alchemy/Railway";
31
32export const Site = Railway.Project("Site");
33```
34
35The logical id is still `Site`, so Alchemy recognizes it as the same
36Project you deployed in Part 1 — moving declarations between files
37doesn't recreate anything.
38
39## Update the Stack
40
41Import the Project and yield it from the Stack:
42
43```diff lang="typescript"
44// alchemy.run.ts
45import * as Alchemy from "alchemy";
46import * as Railway from "alchemy/Railway";
47import * as Effect from "effect/Effect";
48+import { Site } from "./src/project.ts";
49
50export default Alchemy.Stack(
51 "MyApp",
52 {
53 providers: Railway.providers(),
54 state: Alchemy.localState(),
55 },
56 Effect.gen(function* () {
57- const site = yield* Railway.Project("Site");
58+ const site = yield* Site;
59
60 return {
61 name: site.name,
62 projectId: site.projectId,
63 url: site.url,
64 };
65 }),
66);
67```
68
69## Create the Service file
70
71A Service in Alchemy is a class — it has both an infrastructure
72definition and a runtime implementation expressed as an Effect.
73Create `src/api.ts` with the smallest possible declaration:
74
75```typescript
76// src/api.ts
77import * as Railway from "alchemy/Railway";
78import * as Effect from "effect/Effect";
79import { Site } from "./project.ts";
80
81export default class Api extends Railway.Service<Api>()(
82 "Api",
83 {
84 project: Site,
85 main: import.meta.url,
86 },
87 Effect.gen(function* () {
88 return {};
89 }),
90) {}
91```
92
93The `<Api>` type argument plus the empty `()` is a one-time bit of
94ceremony — it lets TypeScript reason about `Api` as a typed handle.
95`project: Site` says which Project this Service runs in, and
96`main: import.meta.url` tells Alchemy this same file is the bundle
97entrypoint.
98
99## Serve HTTP with `fetch`
100
101Add a `fetch` field — Alchemy treats anything returned from the
102`Effect.gen` block as the runtime API, and `fetch` specifically is
103wired to an HTTP server listening on the Service's port:
104
105```diff lang="typescript"
106// src/api.ts
107import * as Railway from "alchemy/Railway";
108import * as Effect from "effect/Effect";
109+import * as HttpServerResponse from "effect/http/HttpServerResponse";
110import { Site } from "./project.ts";
111
112export default class Api extends Railway.Service<Api>()(
113 "Api",
114 {
115 project: Site,
116 main: import.meta.url,
117 },
118 Effect.gen(function* () {
119- return {};
120+ return {
121+ fetch: Effect.succeed(HttpServerResponse.text("Hello from Railway!")),
122+ };
123 }),
124) {}
125```
126
127`HttpServerResponse` is the same `effect/http` API used on
128every other Alchemy runtime — the handler you write here would run
129unchanged on Cloudflare Workers or AWS Lambda.
130
131:::note
132Notice the "Effect returning an Effect" pattern. The outer generator
133is the **Construction phase** — it runs at deploy time and at process
134startup. The inner `fetch` generator is the **Runtime phase** — it
135runs per request.
136:::
137
138## Pin a region
139
140Railway Services live in a [region](/railway/compute/regions). Omit
141it and Railway picks a default. Set it explicitly so the Volume you
142add in Part 3 can match:
143
144```diff lang="typescript"
145export default class Api extends Railway.Service<Api>()(
146 "Api",
147- { project: Site, main: import.meta.url },
148+ { project: Site, main: import.meta.url, region: "us-west2" },
149```
150
151Updating `region` later is in place.
152
153## Set the port
154
155Give the Service an explicit port so Railway's edge knows where to
156forward:
157
158```diff lang="typescript"
159export default class Api extends Railway.Service<Api>()(
160 "Api",
161- { project: Site, main: import.meta.url, region: "us-west2" },
162+ { project: Site, main: import.meta.url, region: "us-west2", port: 3000 },
163```
164
165The port is written to the process environment as `PORT` and used as
166the generated domain's `targetPort`. Alchemy publishes HTTPS on
167Railway's edge in front of it.
168
169## Add a health route
170
171Add `/health` so you can probe the process:
172
173```diff lang="typescript"
174// src/api.ts
175+import { HttpServerRequest } from "effect/http/HttpServerRequest";
176// ...
177 return {
178- fetch: Effect.succeed(HttpServerResponse.text("Hello from Railway!")),
179+ fetch: Effect.gen(function* () {
180+ const request = yield* HttpServerRequest;
181+ const url = new URL(request.url, "http://service");
182+ if (url.pathname === "/health") {
183+ return HttpServerResponse.json({ ok: true });
184+ }
185+ return HttpServerResponse.text("Hello from Railway!");
186+ }),
187 };
188```
189
190## Wire the Service into the Stack
191
192The `Api` class is just a typed identifier — yielding it inside the
193Stack's Effect is what registers the resource and starts the deploy:
194
195```diff lang="typescript"
196// alchemy.run.ts
197import * as Alchemy from "alchemy";
198import * as Railway from "alchemy/Railway";
199import * as Effect from "effect/Effect";
200+import Api from "./src/api.ts";
201import { Site } from "./src/project.ts";
202
203export default Alchemy.Stack(
204 "MyApp",
205 {
206 providers: Railway.providers(),
207 state: Alchemy.localState(),
208 },
209 Effect.gen(function* () {
210 const site = yield* Site;
211+ const api = yield* Api;
212
213 return {
214 name: site.name,
215 projectId: site.projectId,
216- url: site.url,
217+ url: api.url,
218 };
219 }),
220);
221```
222
223Yielding `Api` returns the resolved Service outputs — the service
224id, region, and the public `url`.
225
226## Deploy
227
228<Tabs syncKey="pkgManager">
229 <TabItem label="bun" icon="bun">
230 <Code code="bun alchemy deploy" lang="sh" />
231 </TabItem>
232 <TabItem label="npm" icon="npm">
233 <Code code="npm run alchemy deploy" lang="sh" />
234 </TabItem>
235 <TabItem label="pnpm" icon="pnpm">
236 <Code code="pnpm alchemy deploy" lang="sh" />
237 </TabItem>
238 <TabItem label="yarn" icon="seti:yarn">
239 <Code code="yarn alchemy deploy" lang="sh" />
240 </TabItem>
241</Tabs>
242
243<Terminal content={`[u]Plan[/u]: [g]1 to create[/g]
244
245[g]+[/g] [b]Api[/b] [d](Railway.Service)[/d]
246[d]•[/d] [b]Site[/b] [d](Railway.Project)[/d]
247
248Proceed?
249◉ Yes ○ No
250[g]✓[/g] [b]Api[/b] [d](Railway.Service)[/d] [g]created[/g]
251{
252[s2]name: "myapp-site-dev-a1b2c3d4",
253[s2]projectId: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
254[s2]url: "https://myapp-api-dev-a1b2c3d4.up.railway.app",
255}`} />
256
257A lot just happened:
258
2591. `src/api.ts` was bundled with Rolldown into a single ESM file.
2602. Alchemy generated a Dockerfile (`FROM oven/bun:1`) and uploaded
261 the context. Railway built the image.
2623. It created the Service, attached a `*.up.railway.app` domain, and
263 pointed Railway's edge at port 3000.
264
265## Try it out
266
267```sh
268curl https://myapp-api-dev-a1b2c3d4.up.railway.app
269# → Hello from Railway!
270```
271
272## Ship a change
273
274Edit the greeting in `src/api.ts` and deploy again:
275
276<Terminal content={`[u]Plan[/u]: [y]1 to update[/y]
277
278[y]~[/y] [b]Api[/b] [d](Railway.Service)[/d]
279
280Proceed?
281◉ Yes ○ No
282[g]✓[/g] [b]Api[/b] [d](Railway.Service)[/d] [y]updated[/y]`} />
283
284Alchemy hashes the bundle: if your code didn't change, the Service
285is a no-op; if it did, a new image is built, pushed, and the Service
286updated in place.
287
288## Recap
289
290You now have:
291
292- An HTTP Service running as a Railway container in `us-west2`
293- A public `https://{name}.up.railway.app` URL
294- A code-hash-based update loop — edit, deploy, new image
295
296In [Part 3](/railway/tutorial/part-3), you'll attach a Volume and
297persist data across deploys.