1---
2title: Services
3description: An Effect program running in Fly Machines in its own Fly App. Scale it with count.
4---
5
6A [`Service`](/providers/fly/service) is an Effect program running
7in Fly.io [Machines](/fly/compute/machines) inside its own Fly App.
8It gets its own `{name}.fly.dev` hostname, addresses, logs, and
9metrics. Set `count` to scale it up or down.
10
11## Declare a Service
12
13A Service is a class. Props describe the Machine. The Effect is the
14program that runs on it.
15
16```typescript
17// src/api.ts
18import * as Fly from "alchemy/Fly";
19import * as Effect from "effect/Effect";
20
21export default class Api extends Fly.Service<Api>()(
22 "Api",
23 { main: import.meta.url },
24 Effect.gen(function* () {
25 return {};
26 }),
27) {}
28```
29
30The Service creates its Fly App. Deleting the Service deletes that
31App.
32
33`main: import.meta.url` is the bundle entrypoint. Alchemy bundles
34this file with Rolldown, builds a Docker image (default
35`node:26-slim`), and pushes it to
36`registry.fly.io/{app}:{id}-{hash}`.
37
38## Serve HTTP with `fetch`
39
40Return `fetch` from the constructor Effect to boot an HTTP server.
41
42```diff lang="typescript"
43import * as Fly from "alchemy/Fly";
44import * as Effect from "effect/Effect";
45+import * as HttpServerResponse from "effect/http/HttpServerResponse";
46
47export default class Api extends Fly.Service<Api>()(
48 "Api",
49 { main: import.meta.url },
50 Effect.gen(function* () {
51- return {};
52+ return {
53+ fetch: Effect.succeed(HttpServerResponse.text("hello")),
54+ };
55 }),
56) {}
57```
58
59Omit `fetch` for a [background service](#background-services).
60
61## Pin a region
62
63Fly Machines live in a [region](/fly/compute/regions). The default
64is `iad`. Pass a list, such as `region: ["iad", "lhr"]`, to run
65`count` Machines in each region behind one hostname; see
66[Several regions, one hostname](/fly/compute/regions#several-regions-one-hostname).
67Changing `region` updates the Service in place.
68
69```diff lang="typescript"
70export default class Api extends Fly.Service<Api>()(
71 "Api",
72- { main: import.meta.url },
73+ { main: import.meta.url, region: "iad" },
74```
75
76## Set the port
77
78`port` is the port the process listens on inside the Machine.
79Alchemy writes it to `PORT`. Default `3000`.
80
81```diff lang="typescript"
82export default class Api extends Fly.Service<Api>()(
83 "Api",
84- { main: import.meta.url, region: "iad" },
85+ { main: import.meta.url, region: "iad", port: 3000 },
86```
87
88## The public URL
89
90A Service is public by default. Alchemy allocates a shared IPv4 and
91an IPv6 on its App (both free), and `api.url` is
92`https://{appName}.fly.dev`.
93
94```typescript
95export default Alchemy.Stack(
96 "MyApp",
97 { providers: Fly.providers(), state: Alchemy.localState() },
98 Effect.gen(function* () {
99 const api = yield* Api;
100 return { url: api.url };
101 }),
102);
103```
104
105`url` is `undefined` when you pass `services: []` (nothing is
106published).
107
108## Every published port
109
110`url` is one endpoint. A Service that publishes several ports has one
111endpoint per port at the same hostname. `endpoints` lists them with
112the port, handlers, and a URL when the port speaks HTTP. Ports that
113only redirect to HTTPS are left out.
114
115```typescript
116export default class Api extends Fly.Service<Api>()(
117 "Api",
118 {
119 main: import.meta.url,
120 services: [
121 {
122 internalPort: 3000,
123 ports: [
124 { port: 80, handlers: ["http"], forceHttps: true },
125 { port: 443, handlers: ["tls", "http"] },
126 ],
127 },
128 { internalPort: 9000, ports: [{ port: 8443, handlers: ["tls", "http"] }] },
129 { internalPort: 7000, ports: [{ port: 7000 }] },
130 ],
131 },
132 Effect.gen(function* () {
133 return {};
134 }),
135) {}
136```
137
138`api.url` is `https://{appName}.fly.dev`. `api.endpoints` has three
139entries: 443 and 8443 with `https://` URLs, and 7000 with no URL,
140because raw TCP has no HTTP address.
141
142:::note[Plain HTTP on other ports needs IPv6]
143The free shared IPv4 serves TLS on any port but plain HTTP only on
144port 80. A plain-HTTP port such as 8080 answers over IPv6 only.
145:::
146
147## A stable hostname
148
149`name` names the Service's App, so it is the `{name}.fly.dev`
150hostname. App names are globally unique across Fly. Omit `name` and
151Alchemy generates one from the stack, stage, and logical ID.
152
153```diff lang="typescript"
154export default class Api extends Fly.Service<Api>()(
155 "Api",
156- { main: import.meta.url, region: "iad", port: 3000 },
157+ { main: import.meta.url, name: "my-api", region: "iad", port: 3000 },
158```
159
160Changing `name` replaces the Service. Fly cannot rename an App, so
161Alchemy creates the Service under the new name, then deletes the old
162App.
163
164## Private Services
165
166`public: false` keeps a Service off the internet. It gets only a
167free [Flycast](/fly/networking#private-services-with-flycast) address
168and publishes plain HTTP on port 80, because Fly issues no
169certificate for `.flycast`. `url` is `undefined`.
170
171```typescript
172// src/users.ts
173export default class Users extends Fly.Service<Users>()(
174 "Users",
175 { main: import.meta.url, public: false },
176 Effect.gen(function* () {
177 return {
178 list: () => Effect.succeed([{ id: "u1", name: "Ada" }]),
179 };
180 }),
181) {}
182```
183
184Other Services call it by [binding it](#call-one-service-from-another).
185Turning `public` on or off updates the Service in place; its App and
186hostname stay the same.
187
188## The private URL
189
190`privateUrl` is where bound callers reach a Service over Fly's private
191network:
192
193- A private Service: `http://{appName}.flycast` (plain HTTP on 80).
194- A public Service: `http://{appName}.flycast:7780`. Its default ports
195 are an HTTPS redirect (80) and HTTPS (443), so Alchemy publishes an
196 extra plain-HTTP port for bound callers, `bindingPort`.
197
198`bindingPort` defaults to `7780`. It answers only requests that arrive
199over Fly's private network. Change it when it collides with another
200port, for example another Service's `bindingPort` in a
201[shared App](#group-services-in-one-app):
202
203```diff lang="typescript"
204export default class Api extends Fly.Service<Api>()(
205 "Api",
206- { main: import.meta.url, port: 3000 },
207+ { main: import.meta.url, port: 3000, bindingPort: 7781 },
208```
209
210A Service that already publishes a plain-HTTP port uses that port
211instead. `privateUrl` is `undefined` when the Service publishes
212nothing (`services: []`).
213
214## Restrict a Service to the stack's network
215
216Fly's default private network spans the organization, so every App
217in the org can resolve a private Service. `network` puts the Service's
218App on its own private network instead. Only Apps on the same network
219can resolve its `privateUrl`.
220`Fly.stackNetwork` yields a network name unique to the stack and
221stage; yield it inside Effect-valued props:
222
223```typescript
224export default class Users extends Fly.Service<Users>()(
225 "Users",
226 Effect.gen(function* () {
227 return {
228 main: import.meta.url,
229 public: false,
230 network: yield* Fly.stackNetwork,
231 };
232 }),
233 /* ... */
234) {}
235```
236
237Give every Service in the stack the same `network`. Changing it
238replaces the Service, and it cannot be combined with `app`.
239[Connect Services](/fly/compute/connecting-services) walks through a
240private backend behind a public gateway.
241
242## Call one Service from another
243
244Bind a Service with `Fly.bindService` in the caller's program. The
245client has the methods the target returns (everything besides `fetch`
246and `run`), typed from its program:
247
248```typescript
249// src/gateway.ts
250import Users from "./users.ts";
251
252export default class Gateway extends Fly.Service<Gateway>()(
253 "Gateway",
254 { main: import.meta.url },
255 Effect.gen(function* () {
256 const users = yield* Fly.bindService(Users);
257 return {
258 fetch: Effect.gen(function* () {
259 return yield* HttpServerResponse.json(yield* users.list());
260 }).pipe(Effect.orDie),
261 };
262 }),
263) {}
264```
265
266Binding makes `Gateway` deploy after `Users` and hands it the address
267and caller token of `Users`. Only Services that bind `Users` receive
268its token, so only they can call its methods. The caller must be on
269the same network as the target; otherwise its deploy fails with
270`Fly.ServiceUnreachable`.
271
272`users.fetch(HttpClientRequest.get("/path"))` sends a request to the
273target's `fetch` routes. `Fly.bindEndpoint(Users, { port: 9000 })`
274binds one other published port and returns its `host`, `port`, `url`,
275and an HTTP `client`.
276
277[Connect Services](/fly/compute/connecting-services) walks through
278several Services on a stack network, the security model, streaming
279methods, `bindEndpoint`, and two Services that bind each other.
280
281## Fly's proxy is the load balancer
282
283There is no LoadBalancer resource. Fly runs an Anycast proxy at the
284edge. Unless you override `services`, Alchemy publishes HTTP 80 and
285HTTPS 443 on that proxy (plain HTTP 80 for a private Service) and
286points them at `port` inside each Machine.
287
288A request to `https://{appName}.fly.dev` lands on Fly's edge. Fly
289terminates TLS on 443, picks one started Machine that published this
290service, and forwards to `port` where `fetch` runs.
291
292## Scale with `count`
293
294`count` is how many Machines to provision. Default `1`. They all
295publish the same proxy service behind `{app}.fly.dev`. Fly's proxy
296picks an available Machine per request. With `autostop: "stop"` or
297`"suspend"`, some provisioned replicas can be idle; `count` is not a
298promise that every process is continuously running. Use `autostart`
299and `minMachinesRunning` for Fly's wake-up and minimum-capacity behavior.
300See [idle capacity during replacement](/fly/compute/deployments#keep-idle-capacity-with-stop-or-suspend).
301
302Each replica gets its own Volume from every
303[`MountVolume`](/fly/data/volumes) binding. Volume-backed Services use
304rolling updates; blue/green does not clone or share those volumes.
305
306```diff lang="typescript"
307export default class Api extends Fly.Service<Api>()(
308 "Api",
309- { main: import.meta.url, region: "iad", port: 3000 },
310+ { main: import.meta.url, region: "iad", count: 3, port: 3000 },
311```
312
313## What a deploy does
314
315Docker must be running.
316
317Alchemy creates the Service's App and its addresses if they are
318missing, then bundles `main` with Rolldown. If the hash matches the
319last deploy, it skips build and push. Otherwise it builds
320`linux/amd64` from `image` (default `node:26-slim`) and pushes to
321`registry.fly.io/{app}:{id}-{hash}`. With the default rolling policy,
322it creates or updates replicas sequentially. For each Machine it starts,
323reconcile waits for configured readiness checks before proceeding.
324
325By default, changed code produces a new image and an in-place Machine update.
326Unchanged desired inputs are a no-op. Opt into `deploy: { strategy: "bluegreen" }` to
327[prepare healthy replacements before retiring the old generation](/fly/compute/deployments).
328Keep one Service declaration. Alchemy manages both generations and uses the
329old Machine's shutdown policy when retiring it. The guide explains readiness,
330traffic overlap, Effect finalizers, idle capacity, and recovery. Raw images and
331external servers must handle their own shutdown.
332
333Override the base image with `image` (must still run Node). Pass
334`services: []` for a process that should not be published.
335
336:::note[Deploy waits for Fly service checks]
337If a Machine has service checks (the default TCP check, or your HTTP
338checks), reconcile waits until Fly reports them `passing` before it
339moves on to the next replica. `started` only means the VM booted.
340The default readiness budget is 60 seconds; set `deploy.healthTimeout`
341for a longer bounded wait. If checks do not pass, deployment fails with
342`Fly.ReplicaChecksNotPassing`, including the last observed check
343results, and later replicas remain unchanged. Earlier updates are
344not automatically rolled back.
345
346Omitting `/health` does not fail the deploy unless you configured an
347HTTP check on that path. A single replica still updates in place and
348can be unavailable during deployment; this is not blue/green deployment.
349:::
350
351## Config
352
353Yield `Config` in the constructor. Alchemy reads the value from the env of
354whoever deploys and writes it onto the Machine.
355
356```typescript
357import * as Config from "effect/Config";
358import * as Redacted from "effect/Redacted";
359
360export default class Api extends Fly.Service<Api>()(
361 "Api",
362 { main: import.meta.url, port: 3000 },
363 Effect.gen(function* () {
364 const apiKey = yield* Config.Redacted("API_KEY");
365
366 return {
367 fetch: Effect.gen(function* () {
368 const token = Redacted.value(apiKey);
369 // ...
370 }),
371 };
372 }),
373) {}
374```
375
376`Config.Redacted("API_KEY")` is `Redacted<string>`. Unwrap with
377`Redacted.value` only where you need the raw string.
378
379Alchemy also injects `PORT` (when `port` is set) and stack metadata.
380For a secret Fly should own and inject into every Machine on an App,
381use [`Fly.Secret`](/fly/data/secrets) and run the Service in that App
382([Group Services in one App](#group-services-in-one-app)).
383
384Service-bound secret preparation establishes a required version floor for
385replacement Machines, not a snapshot of the App vault. Standalone secrets,
386other Services, and runtime writers are not serialized by Machine leases.
387An out-of-band secret update alone does not guarantee a new rollout; use
388an explicit desired-input change when rotation requires replacement.
389See [rotating secrets with your application](/fly/compute/deployments#rotate-secrets-with-your-application).
390
391## Background services
392
393Omit `fetch` and pass `services: []` so Fly does not publish a proxy.
394Return a `run` Effect for a long-running loop:
395
396```typescript
397// src/worker.ts
398import * as Fly from "alchemy/Fly";
399import * as Effect from "effect/Effect";
400
401export default class Worker extends Fly.Service<Worker>()(
402 "Worker",
403 { main: import.meta.url, region: "iad", services: [] },
404 Effect.succeed({ run: Effect.never }),
405) {}
406```
407
408`Effect.never` above only keeps the example alive. Acquire queue connections
409and start real consumers inside `run`, not during the outer initialization
410Effect that also participates in planning. You can return both `fetch` and
411`run` for mixed HTTP and background work.
412
413For blue/green, a private worker also needs a named readiness check and a real
414server to answer it. A `run`-only program does not create a readiness endpoint.
415Cordoning controls proxy traffic, not job acquisition; both generations can
416consume work before promotion.
417
418With blue/green or explicit `shutdown`, the managed bootstrap initiates runtime
419cleanup on SIGTERM/SIGINT while HTTP requests and shared dependencies remain
420alive. The application owns its stop-acquisition barrier and bounded job drain.
421Use ordinary finalizers and a separately owned work scope for jobs that must
422survive worker-loop interruption; no new public shutdown hook is required.
423See the [queue worker cleanup example](/fly/compute/deployments#close-your-queue-worker-on-shutdown).
424
425Fly's restart policy controls what happens when the process exits. Suspension
426is not ordinary process shutdown, so it is not a promise that finalizers run.
427
428## Group Services in one App
429
430Pass `app` to run a Service inside an existing
431[`Fly.App`](/fly/compute/apps) instead of its own. The Services share
432the App's hostname, addresses, and secrets. Each Service still has its
433own Machines, image, env, and lifecycle. Changing `app`, or adding or
434removing it, replaces the Service.
435
436```typescript
437export const Site = Fly.App("Site");
438
439class Api extends Fly.Service<Api>()(
440 "Api",
441 { app: Site, main: import.meta.url, port: 3000 },
442 /* HTTP */
443) {}
444
445class Worker extends Fly.Service<Worker>()(
446 "Worker",
447 { app: Site, main: import.meta.url, services: [] },
448 /* background loop */
449) {}
450```
451
452Group Services when they need something that attaches to a `Fly.App`:
453
454- a [`Fly.Secret`](/fly/data/secrets) or `SecretKey` shared by several
455 Services
456- a custom domain via [`Fly.Certificate`](/fly/networking#use-your-own-hostname)
457- Fly-native control of the App (`fly` CLI, dashboard settings)
458
459Alchemy manages no public addresses for a Service in a shared App.
460Allocate an [`IpAssignment`](/fly/networking#addresses-for-a-shared-app)
461on the App. `url` and `endpoints` follow each Service's own ports, so
462Services in one App tell themselves apart by port: an API on 443 has
463`url` `https://{appName}.fly.dev`, and an admin UI on 8443 has
464`https://{appName}.fly.dev:8443`. `public` and `network` do not apply
465(combining them with `app` fails with `Fly.InvalidServiceProps`).
466
467### Ports in a shared App
468
469Fly's proxy routes an App's traffic by port only, so each Service in
470the App needs its own ports. When two Services publish the same port
471and protocol, the App fails with `Fly.ServicePortConflict` before any
472Machine is created: at plan time when the App already exists,
473otherwise when the App is reconciled. Each Service's deploy also checks
474the Machines already running in the App, including ones created
475outside the stack.
476
477```typescript
478class Api extends Fly.Service<Api>()(
479 "Api",
480 { app: Site, main: import.meta.url, port: 3000 },
481 /* HTTPS on 443 */
482) {}
483
484class Admin extends Fly.Service<Admin>()(
485 "Admin",
486 {
487 app: Site,
488 main: import.meta.url,
489 port: 3000,
490 bindingPort: 7781,
491 services: [
492 { protocol: "tcp", internalPort: 3000, ports: [{ port: 8443, handlers: ["tls", "http"] }] },
493 ],
494 },
495 /* HTTPS on 8443 */
496) {}
497```
498
499The count includes `bindingPort`: two Services in one App need
500distinct `bindingPort`s. A background Service with `services: []`
501publishes nothing and never conflicts.
502
503### Bind a Service in a shared App
504
505A Service in a shared App is bindable too. Its deploy adds a Flycast
506address to the App, and its `privateUrl` is
507`http://{appName}.flycast` on its own plain-HTTP port or its
508`bindingPort`. Callers must be on the App's network. A Service in a
509shared App cannot be part of a
510[binding cycle](/fly/compute/connecting-services#two-services-that-bind-each-other);
511only Services that own their App can bind each other.
512
513## Logs
514
515Machine logs live in the Fly dashboard and `fly logs -a {appName}`.
516`alchemy logs` (including `--tail`) doesn't support Fly Services yet.
517
518## Where next
519
520The [tutorial](/fly/tutorial/part-1) builds a Service step by step.
521[Connect Services](/fly/compute/connecting-services) binds several
522Services to each other on a private network.
523[Networking](/fly/networking) covers public and private Services.
524[Sprites](/fly/compute/sprites) are org-scoped sandboxes that
525hibernate — no App, no Docker image.
526[Regions](/fly/compute/regions) lists codes and how Volumes follow.
527[Volumes](/fly/data/volumes) covers `MountVolume`.
528[Postgres](/fly/data/postgres) binds with `ConnectPostgres`.
529[Redis](/fly/data/redis) binds with `ReadWriteRedis`.
530[Tigris](/fly/data/tigris) binds with `PutObject` / `GetObject`.
531[Secrets](/fly/data/secrets) covers `Config.Redacted` and `Fly.Secret`.
532The [`Service` reference](/providers/fly/service) lists every prop.