Hetzner
Host your cluster on Hetzner Cloud: Postgres runs on a Server with a Volume, and each runner runs on a Server of its own, all on a private Network. A Load Balancer serves the pool’s gateway, so a Hetzner Service or anything else calls the rooms by id.
import * as ClusterHetzner from "@alchemy.run/effect-cluster/Hetzner";import * as Layer from "effect/Layer";
export const Database = ClusterHetzner.Database("Cluster");
export const Default = ClusterHetzner.ServerPool("Default", { replicas: 2 }).pipe( Layer.provide(Database),);Install @alchemy.run/hetzner and finish Hetzner setup.
- data
- Volume
The database
Section titled “The database”ClusterHetzner.Database creates a private Network and runs Postgres in Docker on a Server with a Volume. Its firewall drops every connection to its public address; runners reach it over the Network.
import * as ClusterHetzner from "@alchemy.run/effect-cluster/Hetzner";
export const Database = ClusterHetzner.Database("Cluster", { location: "fsn1", serverType: "cx33", diskSize: 50,});The pool
Section titled “The pool”ClusterHetzner.ServerPool creates one Server per runner on the Network, and runs the pool’s program on each as a Hetzner.Service. Runners reach each other over the Network; their firewall only lets SSH in, which deploys them. The Load Balancer finds the Servers by label and checks each at /health.
Call it from a Hetzner Service
Section titled “Call it from a Hetzner Service”import * as Hetzner from "@alchemy.run/hetzner";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/http/HttpServerResponse";import { Room } from "./Room.ts";
export const ApiServer = Hetzner.Server("ApiServer", { serverType: "cx23", image: "ubuntu-24.04", location: "nbg1",});
export default class Api extends Hetzner.Service<Api>()( "Api", { server: ApiServer, main: import.meta.url }, Effect.gen(function* () { const rooms = yield* Room; return { fetch: Effect.gen(function* () { const lobby = yield* rooms.getByName("lobby"); return yield* HttpServerResponse.json({ members: yield* lobby.Join({ name: "sam" }) }); }).pipe(Effect.orDie), }; }),) {}Every option, with its default, is in the reference: Database and ServerPool.