Hetzner
Host your Durable Objects on Hetzner Cloud, and the control plane runs on Servers with a Volume each, behind a private Load Balancer and an HTTPS edge. Each node in a pool is a Server of its own. It’s where Part 4 of the tutorial runs the rooms.
import * as RivetHetzner from "@alchemy.run/rivet/Hetzner";import * as Layer from "effect/Layer";
export const Cluster = RivetHetzner.ServerControlPlane("Rivet");
export const Default = RivetHetzner.ServerPool("Default").pipe(Layer.provide(Cluster));Install @alchemy.run/hetzner and finish Hetzner setup.
The control plane
Section titled “The control plane”You get one engine on a Server, with its data on a Volume, behind a Load Balancer on the private Network. A small edge Server runs Caddy in front of it and serves the engines over HTTPS at its own address under sslip.io, like https://203-0-113-7.sslip.io, with a certificate from Let’s Encrypt, so you need no domain. A firewall blocks every other connection to the engines.
export const Cluster = RivetHetzner.ServerControlPlane("Rivet", { location: "fsn1", serverType: "cx33", diskSize: 40,});Run more engines
Section titled “Run more engines”More engines share object storage, so objectStorage is required. Hetzner Object Storage works:
export const Cluster = RivetHetzner.ServerControlPlane("Rivet", { instances: 3, objectStorage: { kind: "s3", bucket: "my-rivet-data", endpoint: "https://nbg1.your-objectstorage.com", region: "nbg1", accessKeyId: Config.String("HETZNER_S3_ACCESS_KEY_ID"), secretAccessKey: Config.Redacted("HETZNER_S3_SECRET_ACCESS_KEY"), },});Create the bucket and its keys in the Hetzner console first.
The pool
Section titled “The pool”export const Default = RivetHetzner.ServerPool("Default", { replicas: 2, serverType: "cx23" }).pipe( Layer.provide(Cluster),);Each node’s firewall only allows SSH, which Alchemy uses to deploy it.
Rivet Services
Section titled “Rivet Services”ServicesPool runs Rivet Services on a Server of its own, behind a firewall that drops every inbound connection:
export const Services = RivetHetzner.ServicesPool("Services").pipe(Layer.provide(Cluster));Call it from a Hetzner Service
Section titled “Call it from a Hetzner Service”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 visitors = yield* rooms.getByName("lobby").join(); return yield* HttpServerResponse.json({ visitors }); }), }; }),) {}Part 4 of the tutorial deploys this.
Every option, with its default, is in the reference: ServerControlPlane, ServerPool and ServicesPool.