Part 1: Your First Server
In this first part you’ll install Alchemy and Effect, create a Stack with a Hetzner Cloud Server, deploy it, and SSH in — all in under five minutes.
Prerequisites
Section titled “Prerequisites”- Bun (recommended) or Node.js 22+
- A Hetzner project and an API token — see Setup if you haven’t created those yet
Create a project
Section titled “Create a project”Start with an empty directory and initialize a package.json:
mkdir my-app && cd my-app && bun init -ymkdir my-app && cd my-app && npm init -ymkdir my-app && cd my-app && pnpm initmkdir my-app && cd my-app && yarn init -yInstall dependencies
Section titled “Install dependencies”Install alchemy@latest and effect@rc:
bun add "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"npm install "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"pnpm add "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"yarn add "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"Create the Stack
Section titled “Create the Stack”Every Alchemy program starts with a Stack — a collection of
Resources managed by Providers with state tracked between deploys.
Create an alchemy.run.ts file:
import * as Alchemy from "alchemy";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";
export default Alchemy.Stack( "MyApp", {Error ts(2345) ― providers: Layer.empty, }, Effect.gen(function* () { // we'll add resources here next }),);TypeScript is unhappy: the state property is required. Every Stack
needs a state store so Alchemy can persist resource state between
deploys and compute diffs against your infrastructure.
Configure state
Section titled “Configure state”Hetzner has no state backend of its own, so we’ll keep state on disk
with Alchemy.localState() — it writes to .alchemy/ next to your
code, no setup required:
import * as Alchemy from "alchemy";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";
export default Alchemy.Stack( "MyApp", { providers: Layer.empty, state: Alchemy.localState(), }, Effect.gen(function* () { // we'll add resources here next }),);Add a Server
Section titled “Add a Server”Resources represent cloud infrastructure. Each resource is
yield*-ed inside the Stack’s Effect generator.
Let’s declare a Server — Hetzner’s virtual machine — and observe the type error:
import * as Alchemy from "alchemy";import * as Hetzner from "alchemy/Hetzner";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";
export default Alchemy.Stack( "MyApp", { providers: Layer.empty,Error ts(2322) ― state: Alchemy.localState(), }, Effect.gen(function* () { const server = yield* Hetzner.Server("box", { serverType: "cx22", image: "ubuntu-24.04", location: "nbg1", }); }),);The props map directly onto Hetzner’s console: serverType is the
size (cx22 = 2 vCPU / 4 GB), image is the OS, and location is
the datacenter (nbg1 is Nuremberg; fsn1, hel1, ash, hil,
and sin also exist).
TypeScript is telling us that Layer.empty doesn’t provide
Hetzner.Providers — the layer required by Server.
Fix the Providers
Section titled “Fix the Providers”Replace Layer.empty with Hetzner.providers() to resolve the type
error:
import * as Alchemy from "alchemy";import * as Hetzner from "alchemy/Hetzner";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";
export default Alchemy.Stack( "MyApp", { providers: Hetzner.providers(), state: Alchemy.localState(), }, Effect.gen(function* () { const server = yield* Hetzner.Server("box", { serverType: "cx22", image: "ubuntu-24.04", location: "nbg1", }); }),);Now the program type-checks. The providers layer tells Alchemy how to talk to the Hetzner Cloud API, and the type system ensures you never forget to wire it up.
Add your SSH key
Section titled “Add your SSH key”The server will boot without one, but registering your public key
lets you SSH straight in (and stops Hetzner emailing you a root
password). Declare an SshKey and pass it to the server:
Effect.gen(function* () { const key = yield* Hetzner.SshKey("laptop", { publicKey: "ssh-ed25519 AAAA... you@laptop", });
const server = yield* Hetzner.Server("box", { serverType: "cx22", image: "ubuntu-24.04", location: "nbg1", sshKeys: [key], });}),Paste your own key from ~/.ssh/id_ed25519.pub (create one with
ssh-keygen -t ed25519 if you don’t have it). Passing the resolved
key to sshKeys is your first resource reference — Alchemy
sees the dependency and orders the deploy so the key exists before
the server boots.
Return Stack outputs
Section titled “Return Stack outputs”Stack outputs let you see important values after a deploy. Return an object from the generator to expose the server’s public addresses:
const server = yield* Hetzner.Server("box", { serverType: "cx22", image: "ubuntu-24.04", location: "nbg1", sshKeys: [key], });
return { ipv4: server.ipv4, ipv6: server.ipv6, };}),Deploy
Section titled “Deploy”Run alchemy deploy to create the key and the server:
bun alchemy deploynpm run alchemy deploypnpm alchemy deployyarn alchemy deployThe first time you deploy, Alchemy prompts for Hetzner credentials —
paste the API token you generated in Setup. The
token is verified and saved to your default
profile, so you won’t be asked again.
Plan: 2 to create + laptop (Hetzner.SshKey) + box (Hetzner.Server) Proceed? ◉ Yes ○ No ✓ laptop (Hetzner.SshKey) created ✓ box (Hetzner.Server) created { ipv4: "203.0.113.10", ipv6: "2001:db8::1", }
Alchemy shows a plan, asks for confirmation, creates both resources, and prints the stack outputs. A Hetzner server boots in about ten seconds.
Verify it worked
Section titled “Verify it worked”The server is listed in your project in the Hetzner Cloud Console, and you can SSH straight in:
ssh root@203.0.113.10Run alchemy deploy again. Because nothing changed, both resources
show as no-ops:
Plan: no changes { ipv4: "203.0.113.10", ipv6: "2001:db8::1", }
This is the core loop — declare resources in code, deploy, and Alchemy figures out what changed.
You now have:
- An
alchemy.run.tswith a Stack, an SSH key, and a Server - A live VM running Ubuntu in Nuremberg, booted in seconds
- Stack outputs showing its public IPv4 and IPv6 addresses
In Part 2, you’ll deploy code to this
server — a Hetzner.Service that serves HTTP without you ever
writing a systemd unit or a Dockerfile.