1---
2title: Services
3description: A container in a Railway Project — a public image, or an Effect program Railway builds from a generated Dockerfile.
4---
5
6A [`Service`](/providers/railway/service) is a container in a
7Railway [Project](/railway/compute/projects). Point it at a public
8image (`hashicorp/http-echo`), an Effect program (`main`), or a
9GitHub repo (`repo` + `branch`). Several Services
10share one Project. Canvas Functions — Effect-native or one TypeScript file, no
11Docker, no registry — are [`Railway.Function`](/railway/compute/functions).
12
13## Image service
14
15Pass `image` without `main`. Railway pulls the image and runs it.
16No Docker. No registry. `url` is the generated `*.up.railway.app`
17hostname.
18
19```typescript
20const site = yield* Railway.Project("Site");
21const api = yield* Railway.Service("Api", {
22 project: site,
23 image: "hashicorp/http-echo",
24 port: 5678,
25});
26```
27
28`project` is the parent [Project](/railway/compute/projects). Pass
29the declaration directly, yielded or module-scope. Changing `project`
30replaces the Service.
31
32## Declare an Effect-native Service
33
34A Service is a class. Props describe the container. The Effect is
35the program that runs in it.
36
37```typescript
38// src/api.ts
39import * as Railway from "alchemy/Railway";
40import * as Effect from "effect/Effect";
41import { Site } from "./project.ts";
42
43export default class Api extends Railway.Service<Api>()(
44 "Api",
45 {
46 project: Site,
47 main: import.meta.url,
48 },
49 Effect.gen(function* () {
50 return {};
51 }),
52) {}
53```
54
55`main: import.meta.url` is the bundle entrypoint. Alchemy bundles
56this file with Rolldown, generates a Dockerfile (default
57`FROM oven/bun:1`), and uploads the context. Railway builds the
58image.
59
60## Serve HTTP with `fetch`
61
62Return `fetch` from the constructor Effect to boot an HTTP server.
63
64```diff lang="typescript"
65import * as Railway from "alchemy/Railway";
66import * as Effect from "effect/Effect";
67+import * as HttpServerResponse from "effect/http/HttpServerResponse";
68import { Site } from "./project.ts";
69
70export default class Api extends Railway.Service<Api>()(
71 "Api",
72 {
73 project: Site,
74 main: import.meta.url,
75 },
76 Effect.gen(function* () {
77- return {};
78+ return {
79+ fetch: Effect.succeed(HttpServerResponse.text("hello")),
80+ };
81 }),
82) {}
83```
84
85Omit `fetch` for a [background service](#background-services).
86
87## Schemaless RPC
88
89Return methods next to `fetch`. Bind the class from another Service
90or Function with `bindService` / `bindFunction`. Calls stay on
91`{name}.railway.internal` with a shared token. Public
92`*.up.railway.app` requests to `/__rpc__/*` get 401.
93
94```typescript
95export default class Query extends Railway.Service<Query>()(
96 "Query",
97 { project: Site, main: import.meta.url },
98 Effect.gen(function* () {
99 return {
100 greet: (name: string) => Effect.succeed(`hello ${name}`),
101 };
102 }),
103) {}
104```
105
106```typescript
107const query = yield* Railway.bindService(Query);
108const greeting = yield* query.greet("sam");
109```
110
111See [Functions](/railway/compute/functions#schemaless-rpc) for the
112Function side of the same stub.
113
114## Pin a region
115
116Railway picks a default region if you omit `region`. Updating it
117is in place. See [Regions](/railway/compute/regions).
118
119```diff lang="typescript"
120export default class Api extends Railway.Service<Api>()(
121 "Api",
122- { project: Site, main: import.meta.url },
123+ { project: Site, main: import.meta.url, region: "us-west2" },
124```
125
126## Set the port
127
128`port` is the port the process listens on. Alchemy writes it to
129`PORT` and uses it as the generated `*.up.railway.app` `targetPort`.
130Default `3000` for Effect-native Services. Pass `5678` for
131`hashicorp/http-echo`.
132
133```diff lang="typescript"
134export default class Api extends Railway.Service<Api>()(
135 "Api",
136- { project: Site, main: import.meta.url, region: "us-west2" },
137+ { project: Site, main: import.meta.url, region: "us-west2", port: 3000 },
138```
139
140## The public URL
141
142Yield the Service in the Stack. `api.url` is
143`https://{name}.up.railway.app`. Alchemy creates that hostname with
144`serviceDomainCreate`.
145
146```typescript
147export default Alchemy.Stack(
148 "MyApp",
149 { providers: Railway.providers(), state: Alchemy.localState() },
150 Effect.gen(function* () {
151 const api = yield* Api;
152 return { url: api.url };
153 }),
154);
155```
156
157There is no IpAssignment resource. Railway's edge terminates TLS on
158443 and forwards to `port`.
159
160## Healthcheck
161
162`healthcheck` / `healthcheckPath` is the HTTP path Railway probes.
163`healthcheckTimeout` is seconds. Railway load-balances public traffic
164across whatever replicas are running. Alchemy does not pin a count.
165
166```typescript
167const api = yield* Railway.Service("Api", {
168 project: site,
169 image: "hashicorp/http-echo",
170 port: 5678,
171 healthcheck: "/health",
172 healthcheckTimeout: 30,
173});
174```
175
176## GitHub source, build, start, cron
177
178`repo` is the third source, next to `image` and `main`. Requires a
179GitHub connection on the Railway account. `buildCommand` /
180`startCommand` are Railway's build/start (distinct from hosted
181`build.install`). `cronSchedule` runs the service on a cron
182expression.
183
184```typescript
185const api = yield* Railway.Service("Api", {
186 project: site,
187 repo: "acme/web",
188 branch: "main",
189 rootDirectory: "apps/api",
190 buildCommand: "pnpm build",
191 startCommand: "pnpm start",
192 healthcheck: "/health",
193});
194```
195
196## Canvas groups
197
198[`Railway.Group`](/providers/railway/group) organizes services,
199databases, volumes, and buckets on the canvas. IaC:
200`group("Backend", [api, worker, db])`.
201
202```typescript
203const backend = yield* Railway.Group("Backend", {
204 project: site,
205 resources: [api, worker, db],
206});
207```
208
209## What a deploy does
210
211For an image Service, Alchemy sets `source.image` and deploys with
212`serviceInstanceDeployV2`. Docker is not required.
213
214For an Effect-native Service, Alchemy bundles `main` with Rolldown
215and generates a Dockerfile (`FROM oven/bun:1` unless you pass
216`image`). If the hash matches the last successful deploy, it skips
217the upload. Otherwise it tars the context and uploads it the same
218way `railway up` does. Railway builds and deploys. No local Docker
219daemon. No registry.
220
221Changed code is a new Railway build and an in-place update.
222Unchanged code is a no-op.
223
224`build.install: ["pg"]` ships `pg` unbundled. Rolldown's CJS interop
225turns `pg.Client` into a namespace (`The superclass is not a
226constructor`) if you bundle it.
227
228```typescript
229export default class Api extends Railway.Service<Api>()(
230 "Api",
231 {
232 project: Site,
233 main: import.meta.url,
234 build: { install: ["pg"] },
235 },
236 Effect.gen(function* () {
237 return {
238 fetch: Effect.succeed(HttpServerResponse.text("hello")),
239 };
240 }),
241) {}
242```
243
244Override the base image with `image` (must still run bun when `main`
245is set).
246
247## Config
248
249Yield `Config` in the constructor. Alchemy reads the value from the env of
250whoever deploys and writes it onto the Service. Do not pass
251`env: { ... }` on a Service unless you are packing a known-plain
252value.
253
254```typescript
255import * as Config from "effect/Config";
256import * as Redacted from "effect/Redacted";
257
258export default class Api extends Railway.Service<Api>()(
259 "Api",
260 {
261 project: Site,
262 main: import.meta.url,
263 port: 3000,
264 },
265 Effect.gen(function* () {
266 const apiKey = yield* Config.Redacted("API_KEY");
267
268 return {
269 fetch: Effect.gen(function* () {
270 const token = Redacted.value(apiKey);
271 // ...
272 }),
273 };
274 }),
275) {}
276```
277
278`Config.Redacted("API_KEY")` is `Redacted<string>`. Unwrap with
279`Redacted.value` only where you need the raw string.
280
281Alchemy also injects `PORT` (when `port` is set) and stack metadata.
282For a secret Railway should own and inject into services, use
283[`Railway.Variable`](/railway/data/variables).
284
285## Background services
286
287Omit `fetch`. Use `ServerHost.run` for a long-running loop:
288
289```typescript
290// src/worker.ts
291import * as Railway from "alchemy/Railway";
292import { ServerHost } from "alchemy/Server";
293import * as Effect from "effect/Effect";
294import { Site } from "./project.ts";
295
296export default class Worker extends Railway.Service<Worker>()(
297 "Worker",
298 {
299 project: Site,
300 main: import.meta.url,
301 region: "us-west2",
302 },
303 Effect.gen(function* () {
304 const host = yield* ServerHost;
305
306 yield* host.run(
307 Effect.gen(function* () {
308 return yield* Effect.never;
309 }).pipe(Effect.orDie),
310 );
311 }),
312) {}
313```
314
315If the process exits, Railway restarts it.
316
317## Multiple Services, one Project
318
319Each Service has its own image, env, and lifecycle. Point several
320at the same `project`.
321
322```typescript
323class Api extends Railway.Service<Api>()(
324 "Api",
325 { project: Site, main: import.meta.url, port: 3000 },
326 /* HTTP */
327) {}
328
329class Worker extends Railway.Service<Worker>()(
330 "Worker",
331 { project: Site, main: import.meta.url },
332 /* mounts a disk, writes files */
333) {}
334```
335
336A Volume attaches to one Service. See
337[Volumes](/railway/data/volumes).
338
339## Logs
340
341Service logs live in the Railway dashboard. `alchemy logs` (including
342`--tail`) doesn't support Railway Services yet.
343
344## Where next
345
346The [tutorial](/railway/tutorial/part-2) builds a Service step by
347step. [Functions](/railway/compute/functions) covers Effect-native
348and canvas Functions, including [schemaless RPC](/railway/compute/functions#schemaless-rpc).
349[Volumes](/railway/data/volumes) covers `MountVolume`.
350[Postgres](/railway/data/postgres) binds with `ConnectPostgres`.
351[MySQL](/railway/data/mysql) and [Mongo](/railway/data/mongo) are the
352same shape.
353[Redis](/railway/data/redis) binds with `ReadWriteRedis`.
354[Buckets](/railway/data/buckets) bind with `PutObject` / `GetObject`.
355[Variables](/railway/data/variables) covers `Config.Redacted`,
356`Railway.Variable`, and `Railway.ref`.
357The [`Service` reference](/providers/railway/service) lists every
358prop.
359[Example: railway-project](https://github.com/alchemy-run/alchemy/tree/main/examples/railway-project)
360is an image Service.
361[Example: railway-service](https://github.com/alchemy-run/alchemy/tree/main/examples/railway-service)
362is an Effect-native Service bound to Postgres, MySQL, Redis, an
363Effect Function, and a Group.