1---
2title: Services
3description: Deploy Effect programs to Hetzner Servers as supervised systemd units — HTTP handlers, background workers, env & config, updates, and logs.
4---
5
6A [`Service`](/providers/hetzner/service) is your code on a
7[Server](/hetzner/compute/servers): an Effect program that Alchemy
8bundles with Rolldown, copies over SSH, and runs under systemd with
9`Restart=always`. You never write a unit file, a Dockerfile, or a
10deploy script — and several Services can share one Server.
11
12## Declare a Service
13
14A Service is a class with an infrastructure definition and a runtime
15implementation:
16
17```typescript
18// src/api.ts
19import * as Hetzner from "alchemy/Hetzner";
20import * as Effect from "effect/Effect";
21import * as HttpServerResponse from "effect/http/HttpServerResponse";
22import { Box } from "./server.ts";
23
24export default class Api extends Hetzner.Service<Api>()(
25 "Api",
26 { server: Box, main: import.meta.url, port: 3000 },
27 Effect.gen(function* () {
28 return {
29 fetch: Effect.succeed(HttpServerResponse.text("hello")),
30 };
31 }),
32) {}
33```
34
35- `server` — the machine to run on (a `Hetzner.Server`, yielded or
36 module-scope). Changing it **replaces** the Service.
37- `main: import.meta.url` — this file is the bundle entrypoint.
38- `port` — written to the `PORT` env var and used to build the
39 `url` attribute (`http://<server-ipv4>:<port>`). Default `3000`.
40- `fetch` — returning it from the constructor Effect boots an HTTP server
41 on that port; omit it for a [background service](#background-services).
42
43The resolved resource exposes `url`, `port`, `unitName`, `serverId`,
44and the bundle's `code.hash`.
45
46## What a deploy does
47
48One SSH session, using the Server's Alchemy-managed deploy key:
49
501. Bundle `main` with Rolldown into a single ESM file (a generated
51 bootstrap wires up the Bun HTTP server, logging, and config from
52 the environment).
532. Attach and mount any [bound Volumes](/hetzner/data/volumes).
543. Unpack the bundle to `/opt/<unitName>/`, write the env file, and
55 install the systemd unit:
56
57 ```ini
58 [Service]
59 WorkingDirectory=/opt/<unitName>
60 EnvironmentFile=-/opt/<unitName>/env
61 ExecStart=/root/.bun/bin/bun --no-install /opt/<unitName>/index.mjs
62 Restart=always
63 RestartSec=5
64 ```
65
664. `systemctl enable --now`, restart, and wait for the unit to be
67 active — and, when a `port` is declared, for
68 `http://127.0.0.1:<port>/health` to answer.
69
70On later deploys Alchemy compares the bundle's content hash against
71what's deployed: unchanged code is a no-op; changed code is copied
72up and the unit restarted, a few seconds end to end.
73
74:::caution[The `/health` route is load-bearing]
75A Service that declares a `port` **must serve `/health`** — the
76deploy polls it and fails (dumping the unit's journal into the
77output) if it never answers. Keep it trivial:
78
79```typescript
80if (url.pathname === "/health") {
81 return HttpServerResponse.json({ ok: true });
82}
83```
84:::
85
86## Reachability
87
88The `url` is the Server's public IP and the Service's port —
89there's no proxy in between. A fresh Hetzner server has no firewall
90(every port open); once you apply a
91[`Firewall`](/hetzner/networking#firewalls), inbound is default-deny
92and you choose who reaches the port. For a stable front door with
93health checks and TLS, put a
94[Load Balancer](/hetzner/networking#load-balancer) in front and
95allow the port only from the private network.
96
97## Environment & config
98
99Pass static values with `env`; read them at runtime through Effect's
100`Config` (the runtime's `ConfigProvider` is backed by the process
101environment):
102
103```typescript
104import * as Config from "effect/Config";
105
106export default class Api extends Hetzner.Service<Api>()(
107 "Api",
108 {
109 server: Box,
110 main: import.meta.url,
111 port: 3000,
112 env: { GREETING: "hello" },
113 },
114 Effect.gen(function* () {
115 const greeting = yield* Config.String("GREETING");
116 // ...
117 }),
118) {}
119```
120
121Alchemy also injects `PORT` (when `port` is set) and the stack
122metadata (`ALCHEMY_STACK_NAME`, `ALCHEMY_STAGE`). Values from
123bindings are merged first, so your `env` wins on conflict. See
124[Secrets & Config](/environments/secrets) for handling secret
125values.
126
127## Background services
128
129Omit `port`/`fetch` and use `ServerHost.run` to register a
130long-running loop — a worker, a queue consumer, a cron-ish daemon:
131
132```typescript
133// src/worker.ts
134import * as Hetzner from "alchemy/Hetzner";
135import { ServerHost } from "alchemy/Server";
136import * as Effect from "effect/Effect";
137
138export default class Worker extends Hetzner.Service<Worker>()(
139 "Worker",
140 { server: Box, main: import.meta.url },
141 Effect.gen(function* () {
142 const host = yield* ServerHost;
143
144 yield* host.run(
145 Effect.gen(function* () {
146 // do work forever
147 return yield* Effect.never;
148 }).pipe(Effect.orDie),
149 );
150 }),
151) {}
152```
153
154systemd still supervises the process — if your loop exits or
155crashes, the unit restarts after 5 seconds.
156
157## Multiple Services, one Server
158
159Each Service is its own unit with its own bundle, env file, and
160lifecycle. Point several at the same `server` and share resources
161between them — two Services mounting the same Volume at the same
162path is one attach and one mount:
163
164```typescript
165// Api serves what Worker writes — same Box, same Volume
166class Api extends Hetzner.Service<Api>()(
167 "Api",
168 { server: Box, main: import.meta.url, port: 3000 },
169 /* mounts Data at /data, serves files */
170) {}
171
172class Worker extends Hetzner.Service<Worker>()(
173 "Worker",
174 { server: Box, main: import.meta.url },
175 /* mounts Data at /data, writes files */
176) {}
177```
178
179## Logs
180
181Service logs go to the systemd journal. `alchemy logs`/`alchemy
182tail` don't support Hetzner Services yet — read them over SSH:
183
184```sh
185ssh root@<server-ip> journalctl -u <unitName> -f
186```
187
188The unit name is the Service's `unitName` output (surface it as a
189stack output to keep it handy). Failed deploys already print the
190last 80 journal lines.
191
192## Where next
193
194- [Tutorial part 2](/hetzner/tutorial/part-2) — build up a Service
195 step by step.
196- [Volumes](/hetzner/data/volumes) — persistent storage via the
197 `MountVolume` binding.
198- [Networking](/hetzner/networking) — firewalls and load balancers
199 in front of your Service.
200- [`Service` reference](/providers/hetzner/service) — every prop
201 and attribute.