Services
A Service is an Effect program running
in a Fly.io Machine. Set count to scale
it up or down. Several Services share one
App.
Declare a Service
Section titled “Declare a Service”A Service is a class. Props describe the Machine. The Effect is the program that runs on it.
import * as Fly from "alchemy/Fly";import * as Effect from "effect/Effect";import { Site } from "./app.ts";
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url }, Effect.gen(function* () { return {}; }),) {}app is the parent App. Pass the declaration
directly, yielded or module-scope. Changing app replaces the
Service.
main: import.meta.url is the bundle entrypoint. Alchemy bundles
this file with Rolldown, builds a Docker image (default
node:26-slim), and pushes it to
registry.fly.io/{app}:{id}-{hash}.
Serve HTTP with fetch
Section titled “Serve HTTP with fetch”Return fetch from the constructor Effect to boot an HTTP server.
import * as Fly from "alchemy/Fly";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";import { Site } from "./app.ts";
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url }, Effect.gen(function* () { return {}; return { fetch: Effect.succeed(HttpServerResponse.text("hello")), }; }),) {}Omit fetch for a background service.
Pin a region
Section titled “Pin a region”Fly Machines live in a region. The default
is iad. Changing region replaces the Service.
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url }, { app: Site, main: import.meta.url, region: "iad" },Set the port
Section titled “Set the port”port is the port the process listens on inside the Machine.
Alchemy writes it to PORT. Default 3000.
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, region: "iad" }, { app: Site, main: import.meta.url, region: "iad", port: 3000 },The public URL
Section titled “The public URL”Yield the Service in the Stack. api.url is
https://{appName}.fly.dev. Alchemy does not create this hostname.
It is the parent App’s fly.dev name. The
Service does not get its own URL.
export default Alchemy.Stack( "MyApp", { providers: Fly.providers(), state: Alchemy.localState() }, Effect.gen(function* () { const api = yield* Api; return { url: api.url }; }),);url is undefined when you pass services: [].
Fly’s proxy is the load balancer
Section titled “Fly’s proxy is the load balancer”There is no LoadBalancer resource. Fly runs an Anycast proxy at the
edge. Unless you override services, Alchemy publishes HTTP 80 and
HTTPS 443 on that proxy and points them at port inside each
Machine.
A request to https://{appName}.fly.dev lands on Fly’s edge. Fly
terminates TLS on 443, picks one started Machine that published this
service, and forwards to port where fetch runs.
An address so it answers
Section titled “An address so it answers”{app}.fly.dev does not answer over IPv4 until the App has an
IpAssignment. Allocate a shared Anycast IPv4
on the same App and yield it next to the Service.
export const PublicIp = Fly.IpAssignment("Shared", { app: Site, type: "shared_v4",});Effect.gen(function* () { const api = yield* Api; const ip = yield* PublicIp; return { url: api.url, ip: ip.ip };}),Scale with count
Section titled “Scale with count”count is how many Machines to provision. Default 1. They all
publish the same proxy service behind {app}.fly.dev. Fly’s proxy
picks an available Machine per request. With autostop: "stop" or
"suspend", some provisioned replicas can be idle; count is not a
promise that every process is continuously running. Use autostart
and minMachinesRunning for Fly’s wake-up and minimum-capacity behavior.
See idle capacity during replacement.
Each replica gets its own Volume from every
MountVolume binding. Volume-backed Services use
rolling updates; blue/green does not clone or share those volumes.
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, region: "iad", port: 3000 }, { app: Site, main: import.meta.url, region: "iad", count: 3, port: 3000 },What a deploy does
Section titled “What a deploy does”Docker must be running.
Alchemy bundles main with Rolldown. If the hash matches the last
deploy, it skips build and push. Otherwise it builds linux/amd64
from image (default node:26-slim) and pushes to
registry.fly.io/{app}:{id}-{hash}. With the default rolling policy,
it creates or updates replicas sequentially. For each Machine it starts,
reconcile waits for configured readiness checks before proceeding.
By default, changed code produces a new image and an in-place Machine update.
Unchanged desired inputs are a no-op. Opt into deploy: { strategy: "bluegreen" } to
prepare healthy replacements before retiring the old generation.
Keep one Service declaration. Alchemy manages both generations and uses the
old Machine’s shutdown policy when retiring it. The guide explains readiness,
traffic overlap, Effect finalizers, idle capacity, and recovery. Raw images and
external servers must handle their own shutdown.
Override the base image with image (must still run Node). Pass
services: [] for a process that should not be published.
Config
Section titled “Config”Yield Config in the constructor. Alchemy reads the value from the env of
whoever deploys and writes it onto the Machine. Do not pass
env: { ... } on a Service.
import * as Config from "effect/Config";import * as Redacted from "effect/Redacted";
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, port: 3000 }, Effect.gen(function* () { const apiKey = yield* Config.Redacted("API_KEY");
return { fetch: Effect.gen(function* () { const token = Redacted.value(apiKey); // ... }), }; }),) {}Config.Redacted("API_KEY") is Redacted<string>. Unwrap with
Redacted.value only where you need the raw string.
Alchemy also injects PORT (when port is set) and stack metadata.
For a secret Fly should own and inject into every Machine on the
App, use Fly.Secret.
Service-bound secret preparation establishes a required version floor for replacement Machines, not a snapshot of the App vault. Standalone secrets, other Services, and runtime writers are not serialized by Machine leases. An out-of-band secret update alone does not guarantee a new rollout; use an explicit desired-input change when rotation requires replacement. See rotating secrets with your application.
Background services
Section titled “Background services”Omit fetch and pass services: [] so Fly does not publish a proxy.
Return a run Effect for a long-running loop:
import * as Fly from "alchemy/Fly";import * as Effect from "effect/Effect";import { Site } from "./app.ts";
export default class Worker extends Fly.Service<Worker>()( "Worker", { app: Site, main: import.meta.url, region: "iad", services: [] }, Effect.succeed({ run: Effect.never }),) {}Effect.never above only keeps the example alive. Acquire queue connections
and start real consumers inside run, not during the outer initialization
Effect that also participates in planning. You can return both fetch and
run for mixed HTTP and background work.
For blue/green, a private worker also needs a named readiness check and a real
server to answer it. A run-only program does not create a readiness endpoint.
Cordoning controls proxy traffic, not job acquisition; both generations can
consume work before promotion.
With blue/green or explicit shutdown, the managed bootstrap initiates runtime
cleanup on SIGTERM/SIGINT while HTTP requests and shared dependencies remain
alive. The application owns its stop-acquisition barrier and bounded job drain.
Use ordinary finalizers and a separately owned work scope for jobs that must
survive worker-loop interruption; no new public shutdown hook is required.
See the queue worker cleanup example.
Fly’s restart policy controls what happens when the process exits. Suspension is not ordinary process shutdown, so it is not a promise that finalizers run.
Multiple Services, one App
Section titled “Multiple Services, one App”Each Service has its own Machines, image, env, and lifecycle. Point
several at the same app.
class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, port: 3000 }, /* HTTP */) {}
class Worker extends Fly.Service<Worker>()( "Worker", { app: Site, main: import.meta.url, services: [] }, /* mounts a disk, writes files */) {}A Volume attaches to one Machine. MountVolume with count: 3
creates three disks.
Machine logs live in the Fly dashboard and fly logs. alchemy logs (including --tail) doesn’t support Fly Services yet.
Where next
Section titled “Where next”The tutorial builds a Service step by step.
Sprites are org-scoped sandboxes that
hibernate — no parent App, no Docker image.
Regions lists codes and how Volumes follow.
Volumes covers MountVolume.
Postgres binds with ConnectPostgres.
Redis binds with ReadWriteRedis.
Tigris binds with PutObject / GetObject.
Secrets covers Config.Redacted and Fly.Secret.
The Service reference lists every prop.