1---
2title: "Part 1: Your First Server"
3description: Install Alchemy, create a Stack with a Hetzner Server, and deploy it.
4sidebar:
5 order: 1
6---
7
8import Terminal from "../../../../components/Terminal.astro";
9import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
10import { effectVersion } from "../../../../versions";
11
12export const pkgs = `"alchemy@latest" "effect@${effectVersion}" "@effect/platform-bun@${effectVersion}" "@effect/platform-node@${effectVersion}"`;
13
14In this first part you'll install Alchemy and Effect, create a Stack
15with a Hetzner Cloud Server, deploy it, and SSH in — all in under
16five minutes.
17
18## Prerequisites
19
20- [Bun](https://bun.sh) or Node.js 22+
21- A Hetzner project and an API token — see [Setup](/hetzner/setup)
22 if you haven't created those yet
23
24:::note[Costs]
25Hetzner bills servers by the hour. The `cx22` used in this tutorial
26costs about €4/month — a few cents for the time it takes to finish
27all four parts. `alchemy destroy` tears everything down when you're
28done.
29:::
30
31## Create a project
32
33Start with an empty directory and initialize a `package.json`:
34
35<Tabs syncKey="pkgManager">
36 <TabItem label="bun" icon="bun">
37 <Code code="mkdir my-app && cd my-app && bun init -y" lang="sh" />
38 </TabItem>
39 <TabItem label="npm" icon="npm">
40 <Code code="mkdir my-app && cd my-app && npm init -y" lang="sh" />
41 </TabItem>
42 <TabItem label="pnpm" icon="pnpm">
43 <Code code="mkdir my-app && cd my-app && pnpm init" lang="sh" />
44 </TabItem>
45 <TabItem label="yarn" icon="seti:yarn">
46 <Code code="mkdir my-app && cd my-app && yarn init -y" lang="sh" />
47 </TabItem>
48</Tabs>
49
50## Install dependencies
51
52Install <code>alchemy@latest</code> and <code>effect@{effectVersion}</code>:
53
54<Tabs syncKey="pkgManager">
55 <TabItem label="bun" icon="bun">
56 <Code code={`bun add ${pkgs}`} lang="sh" />
57 </TabItem>
58 <TabItem label="npm" icon="npm">
59 <Code code={`npm install ${pkgs}`} lang="sh" />
60 </TabItem>
61 <TabItem label="pnpm" icon="pnpm">
62 <Code code={`pnpm add ${pkgs}`} lang="sh" />
63 </TabItem>
64 <TabItem label="yarn" icon="seti:yarn">
65 <Code code={`yarn add ${pkgs}`} lang="sh" />
66 </TabItem>
67</Tabs>
68
69## Create the Stack
70
71Every Alchemy program starts with a `Stack` — a collection of
72Resources managed by Providers with state tracked between deploys.
73
74Create an `alchemy.run.ts` file:
75
76```typescript
77// alchemy.run.ts
78import * as Alchemy from "alchemy";
79import * as Effect from "effect/Effect";
80import * as Layer from "effect/Layer";
81
82export default Alchemy.Stack(
83 "MyApp",
84 {
85 // @error: ts(2345) Argument of type '{ providers: Layer.Layer<never, never, never>; }' is not assignable to parameter of type 'StackProps<never>'.
86 // @error: Property 'state' is missing in type '{ providers: Layer.Layer<never, never, never>; }' but required in type 'StackProps<never>'.
87 providers: Layer.empty,
88 },
89 Effect.gen(function* () {
90 // we'll add resources here next
91 }),
92);
93```
94
95TypeScript is unhappy: the `state` property is required. Every Stack
96needs a **state store** so Alchemy can persist resource state between
97deploys and compute diffs against your infrastructure.
98
99## Configure state
100
101Hetzner has no state backend of its own, so we'll keep state on disk
102with `Alchemy.localState()` — it writes to `.alchemy/` next to your
103code, no setup required:
104
105```diff lang="typescript"
106// alchemy.run.ts
107import * as Alchemy from "alchemy";
108import * as Effect from "effect/Effect";
109import * as Layer from "effect/Layer";
110
111export default Alchemy.Stack(
112 "MyApp",
113 {
114 providers: Layer.empty,
115+ state: Alchemy.localState(),
116 },
117 Effect.gen(function* () {
118 // we'll add resources here next
119 }),
120);
121```
122
123:::note[Other state stores]
124Local state is perfect for a solo project. When you need state your
125team and CI can share, swap in `Cloudflare.state()` or `AWS.state()`
126— see [State Store](/state-store).
127:::
128
129## Add a Server
130
131Resources represent cloud infrastructure. Each resource is
132`yield*`-ed inside the Stack's Effect generator.
133
134Let's declare a Server — Hetzner's virtual machine — and observe the
135type error:
136
137```diff lang="typescript"
138// alchemy.run.ts
139import * as Alchemy from "alchemy";
140+import * as Hetzner from "alchemy/Hetzner";
141import * as Effect from "effect/Effect";
142import * as Layer from "effect/Layer";
143
144export default Alchemy.Stack(
145 "MyApp",
146 {
147 providers: Layer.empty,
148 // @error: ts(2322) Type 'Layer<never, never, never>' is not assignable to type 'Layer<NoInfer<Providers>, never, StackServices>'.
149 // @error: Type 'Providers' is not assignable to type 'never'.
150 state: Alchemy.localState(),
151 },
152 Effect.gen(function* () {
153+ const server = yield* Hetzner.Server("box", {
154+ serverType: "cx22",
155+ image: "ubuntu-24.04",
156+ location: "nbg1",
157+ });
158 }),
159);
160```
161
162The props map directly onto Hetzner's console: `serverType` is the
163size (`cx22` = 2 vCPU / 4 GB), `image` is the OS, and `location` is
164the datacenter (`nbg1` is Nuremberg; `fsn1`, `hel1`, `ash`, `hil`,
165and `sin` also exist).
166
167TypeScript is telling us that `Layer.empty` doesn't provide
168`Hetzner.Providers` — the layer required by `Server`.
169
170## Fix the Providers
171
172Replace `Layer.empty` with `Hetzner.providers()` to resolve the type
173error:
174
175```diff lang="typescript"
176// alchemy.run.ts
177import * as Alchemy from "alchemy";
178import * as Hetzner from "alchemy/Hetzner";
179import * as Effect from "effect/Effect";
180-import * as Layer from "effect/Layer";
181
182export default Alchemy.Stack(
183 "MyApp",
184 {
185+ providers: Hetzner.providers(),
186 state: Alchemy.localState(),
187 },
188 Effect.gen(function* () {
189 const server = yield* Hetzner.Server("box", {
190 serverType: "cx22",
191 image: "ubuntu-24.04",
192 location: "nbg1",
193 });
194 }),
195);
196```
197
198Now the program type-checks. The providers layer tells Alchemy how to
199talk to the Hetzner Cloud API, and the type system ensures you never
200forget to wire it up.
201
202## Add your SSH key
203
204The server will boot without one, but registering your public key
205lets you SSH straight in (and stops Hetzner emailing you a root
206password). Declare an `SshKey` and pass it to the server:
207
208```diff lang="typescript"
209Effect.gen(function* () {
210+ const key = yield* Hetzner.SshKey("laptop", {
211+ publicKey: "ssh-ed25519 AAAA... you@laptop",
212+ });
213
214 const server = yield* Hetzner.Server("box", {
215 serverType: "cx22",
216 image: "ubuntu-24.04",
217 location: "nbg1",
218+ sshKeys: [key],
219 });
220}),
221```
222
223Paste your own key from `~/.ssh/id_ed25519.pub` (create one with
224`ssh-keygen -t ed25519` if you don't have it). Passing the resolved
225`key` to `sshKeys` is your first **resource reference** — Alchemy
226sees the dependency and orders the deploy so the key exists before
227the server boots.
228
229## Return Stack outputs
230
231Stack outputs let you see important values after a deploy. Return an
232object from the generator to expose the server's public addresses:
233
234```diff lang="typescript"
235 const server = yield* Hetzner.Server("box", {
236 serverType: "cx22",
237 image: "ubuntu-24.04",
238 location: "nbg1",
239 sshKeys: [key],
240 });
241
242+ return {
243+ ipv4: server.ipv4,
244+ ipv6: server.ipv6,
245+ };
246}),
247```
248
249## Deploy
250
251Run `alchemy deploy` to create the key and the server:
252
253<Tabs syncKey="pkgManager">
254 <TabItem label="bun" icon="bun">
255 <Code code="bun alchemy deploy" lang="sh" />
256 </TabItem>
257 <TabItem label="npm" icon="npm">
258 <Code code="npm run alchemy deploy" lang="sh" />
259 </TabItem>
260 <TabItem label="pnpm" icon="pnpm">
261 <Code code="pnpm alchemy deploy" lang="sh" />
262 </TabItem>
263 <TabItem label="yarn" icon="seti:yarn">
264 <Code code="yarn alchemy deploy" lang="sh" />
265 </TabItem>
266</Tabs>
267
268The first time you deploy, Alchemy prompts for Hetzner credentials —
269paste the API token you generated in [Setup](/hetzner/setup). The
270token is verified and saved to your **`default`**
271[profile](/environments/profiles), so you won't be asked again.
272
273<Terminal content={`[u]Plan[/u]: [g]2 to create[/g]
274
275[g]+[/g] [b]laptop[/b] [d](Hetzner.SshKey)[/d]
276[g]+[/g] [b]box[/b] [d](Hetzner.Server)[/d]
277
278Proceed?
279◉ Yes ○ No
280[g]✓[/g] [b]laptop[/b] [d](Hetzner.SshKey)[/d] [g]created[/g]
281[g]✓[/g] [b]box[/b] [d](Hetzner.Server)[/d] [g]created[/g]
282{
283[s2]ipv4: "203.0.113.10",
284[s2]ipv6: "2001:db8::1",
285}`} />
286
287Alchemy shows a plan, asks for confirmation, creates both resources,
288and prints the stack outputs. A Hetzner server boots in about ten
289seconds.
290
291## Verify it worked
292
293The server is listed in your project in the
294[Hetzner Cloud Console](https://console.hetzner.com), and you can SSH
295straight in:
296
297```sh
298ssh root@203.0.113.10
299```
300
301Run `alchemy deploy` again. Because nothing changed, both resources
302show as no-ops:
303
304<Terminal content={`[u]Plan[/u]: [d]no changes[/d]
305
306{
307[s2]ipv4: "203.0.113.10",
308[s2]ipv6: "2001:db8::1",
309}`} />
310
311This is the core loop — declare resources in code, deploy, and
312Alchemy figures out what changed.
313
314## Recap
315
316You now have:
317
318- An `alchemy.run.ts` with a Stack, an SSH key, and a Server
319- A live VM running Ubuntu in Nuremberg, booted in seconds
320- Stack outputs showing its public IPv4 and IPv6 addresses
321
322In [Part 2](/hetzner/tutorial/part-2), you'll deploy code to this
323server — a `Hetzner.Service` that serves HTTP without you ever
324writing a systemd unit or a Dockerfile.