Effectful services
A Docker.Service can run your own program instead of a pre-built image. Write an Effect that returns a fetch handler, and Alchemy bundles that module and its imports into one file, builds an image on the swarm’s engine that runs it with Bun, and deploys it as the service. Every replica serves HTTP on port and runs any background loops you start. No Dockerfile and no registry are needed while the swarm has one node.
- base
- oven/bun:1
- tag
- content hash
Write the service
Section titled “Write the service”Declare the service in its own module, which is also its main:
import * as Docker from "@alchemy.run/docker";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/http/HttpServerResponse";
export const Swarm = Docker.Swarm("swarm");
export default class Api extends Docker.Service<Api>()( "Api", Effect.gen(function* () { const swarm = yield* Swarm; return { context: swarm, main: import.meta.url, port: 3000, ports: [{ external: 8080, internal: 3000 }], replicas: 2, }; }), Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("hello from the swarm")), }; }),) {}Then deploy it from the Stack:
import * as Alchemy from "alchemy";import * as Docker from "@alchemy.run/docker";import * as Effect from "effect/Effect";import Api from "./src/api.ts";
export default Alchemy.Stack( "MyApp", { providers: Docker.providers(), state: Alchemy.localState(), }, Effect.gen(function* () { const api = yield* Api; return { service: api.name }; }),);port is where the program listens inside each task, and ports publishes it on the swarm, here at localhost:8080. The service takes every other Service prop too: rollouts, networks, secrets and placement work the same.
Run background work
Section titled “Run background work”ServerHost.run starts a loop that runs next to the HTTP server, in every replica:
import * as Docker from "@alchemy.run/docker";import { ServerHost } from "alchemy/Runtime/Process";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/http/HttpServerResponse";import * as Schedule from "effect/Schedule";
export const Swarm = Docker.Swarm("swarm");
export default class Api extends Docker.Service<Api>()( "Api", Effect.gen(function* () { const swarm = yield* Swarm; return { context: swarm, main: import.meta.url, port: 3000, replicas: 2 }; }), Effect.gen(function* () { const host = yield* ServerHost; yield* host.run( Effect.log("heartbeat").pipe( Effect.repeat(Schedule.spaced("30 seconds")), Effect.asVoid, ), ); return { fetch: Effect.succeed(HttpServerResponse.text("hello from the swarm")), }; }),) {}With two replicas, the loop runs twice, once in each task.
Read configuration
Section titled “Read configuration”Read Config while the service is constructed, and the value you deploy with is given to every task:
import * as Docker from "@alchemy.run/docker";import * as Config from "effect/Config";
Effect.gen(function* () { const greeting = yield* Config.String("GREETING"); return { fetch: Effect.succeed(HttpServerResponse.text("hello from the swarm")), fetch: Effect.succeed(HttpServerResponse.text(greeting)), }; }),The value travels in the service’s environment, where docker service inspect can read it. See Secrets for how Config values are bound, and Mount secrets and configs for passwords on a shared swarm.
When it rebuilds
Section titled “When it rebuilds”The image is tagged with a hash of the bundle and its generated Dockerfile. A deploy with no code change builds nothing and leaves the service alone. A change builds a new image and rolls the tasks over to it.
image sets the base image, oven/bun:1 by default. Any base works as long as it can run Bun.
More than one node
Section titled “More than one node”The image is built on the engine of the service’s context, and exists only there. That’s enough for a one-node swarm. Every node of a larger swarm needs to pull the image, so build it with Docker.Image, push it to a registry, and pass that as image instead of main.