1---
2title: Drizzle Postgres on Hetzner
3description: Run a Drizzle API on a Hetzner Server with external Neon Postgres and reviewed SQL migrations.
4---
5
6Alchemy's Hetzner provider has no managed Postgres resource; this guide runs the application on Hetzner and stores its data in **Neon Postgres**. Start with [Hetzner setup](/hetzner/setup), the [Service tutorial](/hetzner/tutorial/part-2), [Neon setup](/neon/setup), and a `src/schema.ts` exporting `Users` from the [Postgres client guide](/sql/drizzle/postgres).
7
8## Install the database dependencies
9
10```sh
11pnpm add drizzle-orm@1.0.0-rc.5-ab785fc @effect/sql-pg pg
12pnpm add -D drizzle-kit@1.0.0-rc.5-ab785fc
13```
14
15The Drizzle versions match the repository's Effect integration.
16
17## Configure migrations
18
19```typescript
20// drizzle.config.ts
21import { defineConfig } from "drizzle-kit";
22
23export default defineConfig({
24 dialect: "postgresql",
25 schema: "./src/schema.ts",
26 out: "./drizzle",
27 dbCredentials: { url: process.env.DATABASE_URL! },
28});
29```
30
31The migration runner uses Neon's direct connection URL; the Service will use its pooled URL.
32
33## Generate after changing the schema
34
35```sh
36pnpm exec drizzle-kit generate
37```
38
39Review the generated SQL and snapshots, including any rename decisions, before applying them.
40
41## Commit the migration
42
43```sh
44git add src/schema.ts drizzle.config.ts drizzle
45git diff --cached
46```
47
48```sh
49git commit -m "Add users migration"
50```
51
52Commit the schema, SQL, and snapshots together; do not generate migration history during deployment.
53
54## Declare the external database
55
56```typescript
57// src/db.ts
58import * as Neon from "alchemy/Neon";
59
60export const Db = Neon.Project("Db", {
61 region: "aws-eu-central-1",
62});
63```
64
65The project's default branch hosts the database; see [Neon connections](/neon/data/connections) for its direct and pooled outputs.
66
67## Register Neon alongside Hetzner
68
69```diff lang="typescript"
70 // alchemy.run.ts
71+import * as Neon from "alchemy/Neon";
72+import * as Layer from "effect/Layer";
73
74- providers: Hetzner.providers(),
75+ providers: Layer.mergeAll(Hetzner.providers(), Neon.providers()),
76```
77
78Keep the tutorial's Server, `yield* Api`, and state backend unchanged.
79
80## Pass the database URL to the Service
81
82Replace the Service's props object in `src/api.ts` with this Effect, importing `Db` from `./db.ts`:
83
84```typescript
85Effect.gen(function* () {
86 const server = yield* Box;
87 const database = yield* Db;
88 return {
89 server,
90 main: import.meta.url,
91 port: 3000,
92 env: { DATABASE_URL: database.pooledConnectionUri },
93 };
94}),
95```
96
97The URL is server-side configuration, not browser configuration; protect the state backend and never expose it as a public Stack output.
98
99## Install the driver on the Server
100
101```diff lang="typescript"
102 return {
103 server,
104 main: import.meta.url,
105 port: 3000,
106+ build: { install: ["pg"] },
107 env: { DATABASE_URL: database.pooledConnectionUri },
108 };
109```
110
111Hetzner's Service packaging installs `pg` beside the bundle, preserving its CommonJS exports.
112
113## Provision before the first migration
114
115```sh
116pnpm alchemy deploy
117```
118
119Keep the tutorial's `/health` route independent of database tables so systemd deployment readiness does not depend on migrations. Allow the Server outbound access to Neon's Postgres endpoint over TLS.
120
121## Apply the committed SQL
122
123```sh
124pnpm exec drizzle-kit migrate
125```
126
127Supply the Neon project's `connectionUri` as secret `DATABASE_URL` in the migration runner, retaining its TLS parameters. This guide keeps Drizzle as the migration owner and does not set Neon's optional `migrations` prop.
128
129## Read the URL at runtime
130
131```diff lang="typescript"
132 // src/api.ts
133+import * as Drizzle from "alchemy/Drizzle/Postgres";
134+import * as Config from "effect/Config";
135
136 Effect.gen(function* () {
137+ const db = yield* Drizzle.Postgres(Config.Redacted("DATABASE_URL"));
138 return {
139```
140
141Add this to the Service's runtime Effect, not its props Effect; the URL and connection are resolved lazily when queried.
142
143## Query after the health check
144
145```diff lang="typescript"
146+import { Users } from "./schema.ts";
147
148 if (url.pathname === "/health") {
149 return yield* HttpServerResponse.json({ ok: true });
150 }
151-return HttpServerResponse.text("Hello from Hetzner!");
152+const users = yield* db.select().from(Users).pipe(Effect.orDie);
153+return yield* HttpServerResponse.json({ users });
154```
155
156The existing request handler supplies the query scope, and `/health` remains independent of the database.
157
158## Deploy the application
159
160```sh
161pnpm alchemy deploy
162```
163
164For later changes, generate, review, and commit the schema, SQL, and snapshots before applying migrations and deploying compatible application code. `alchemy deploy` updates the systemd Service; it does not replace the migration command used here.
165
166## Continue
167
168See the [Neon connection guide](/neon/data/connections), [portable Postgres queries](/sql/drizzle/postgres), and [migration workflow](/sql/drizzle/migrations). The [hetzner-website-vite example](https://github.com/alchemy-run/alchemy/tree/main/examples/hetzner-website-vite) supplies the Neon-to-Service environment wiring; this guide uses committed migrations instead of that example's request-time table creation.