1---
2title: Vite
3description: "Deploy a Vite SPA to Hetzner with Hetzner.Website.Vite — static assets as a systemd unit on a Server, http://{ipv4}:3000, and Vite's own dev server under alchemy dev."
4sidebar:
5 order: 2
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12`Hetzner.Website.Vite` deploys a [Vite](https://vite.dev) project to
13a Hetzner Cloud Server. It runs `vite build` and serves the output
14from a systemd [`Service`](/hetzner/compute/services) on port 3000.
15Omit `server` and Alchemy creates a `cpx12` / `ubuntu-24.04`
16[`Server`](/hetzner/compute/servers) in `fsn1`. Live `url` is
17`http://{ipv4}:3000`. Your `vite.config.*` loads natively — plugins
18included. There is no TLS on the Service and no framework server
19module.
20
21React, Vue, and Solid client SPAs belong here, as do multi-page apps
22that emit one HTML file per route. SSR frameworks that wrap Vite
23deploy through their own composites —
24[`Astro`](/hetzner/frontend/astro),
25[`Nextjs`](/hetzner/frontend/nextjs),
26[`Nuxt`](/hetzner/frontend/nuxt),
27[`ReactRouter`](/hetzner/frontend/react-router),
28[`SolidStart`](/hetzner/frontend/solidstart),
29[`SvelteKit`](/hetzner/frontend/sveltekit),
30[`TanStackStart`](/hetzner/frontend/tanstack-start),
31[`Waku`](/hetzner/frontend/waku),
32[`Octane`](/hetzner/frontend/octane).
33
34## Install
35
36The build integration is not bundled with alchemy. Install
37`@alchemy.run/frontend-frameworks`; the resource loads its `/vite`
38and `/vite/node` exports from your project at deploy time. It is
39only used at build time, so a dev dependency is enough:
40
41<Tabs syncKey="pkgManager">
42 <TabItem label="bun" icon="bun">
43 <Code code={`bun add -d ${devDeps}`} lang="sh" />
44 </TabItem>
45 <TabItem label="npm" icon="npm">
46 <Code code={`npm install -D ${devDeps}`} lang="sh" />
47 </TabItem>
48 <TabItem label="pnpm" icon="pnpm">
49 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
50 </TabItem>
51 <TabItem label="yarn" icon="seti:yarn">
52 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
53 </TabItem>
54</Tabs>
55
56## Configure Vite
57
58Your `vite.config.*` file loads natively — React, Vue, or Solid
59plugins, Tailwind, and any other non-serializable options work
60exactly as they do outside Alchemy. Alchemy merges a programmatic
61config *over* it via the `vite` prop — see
62[Vite configuration](#vite-configuration) below.
63
64```typescript
65// vite.config.ts
66import { defineConfig } from "vite";
67import react from "@vitejs/plugin-react";
68import tailwindcss from "@tailwindcss/vite";
69
70export default defineConfig({
71 plugins: [react(), tailwindcss()],
72});
73```
74
75Do not set a Vite SSR adapter. This composite never creates a
76framework server module — it only ships the client build.
77
78## Declare the Website
79
80Declare the site as a module-level const (rather than inline in the
81Stack):
82
83```typescript
84// alchemy.run.ts
85import * as Hetzner from "alchemy/Hetzner";
86
87export const Website = Hetzner.Website.Vite("Website");
88```
89
90Pass `server` to run the unit on a Server you already have —
91several Services can share one machine; this site always listens on
92port 3000:
93
94```typescript
95export const Box = Hetzner.Server("Box", {
96 serverType: "cpx12",
97 image: "ubuntu-24.04",
98 location: "fsn1",
99});
100
101export const Website = Hetzner.Website.Vite("Website", { server: Box });
102```
103
104## Add it to the Stack
105
106Yield the site from your Stack and return its URL:
107
108```typescript
109// alchemy.run.ts
110import * as Alchemy from "alchemy";
111import * as Effect from "effect/Effect";
112
113export default Alchemy.Stack(
114 "MyViteSite",
115 {
116 providers: Hetzner.providers(),
117 state: Alchemy.localState(),
118 },
119 Effect.gen(function* () {
120 const site = yield* Website;
121 return { url: site.url };
122 }),
123);
124```
125
126On deploy, `url` is `http://{ipv4}:3000`. `server` and `service` are
127the backing resources; they are `undefined` under `alchemy dev`.
128
129See
130[examples/hetzner-website-vite](https://github.com/alchemy-run/alchemy/tree/main/examples/hetzner-website-vite)
131for the checked-in example (API unit and static site on one
132`cpx12`).
133
134## Add environment variables
135
136`env` is process environment — set on `process.env` before
137`vite build` / `alchemy dev`, and on the systemd unit at deploy. It
138is not Worker bindings:
139
140```diff lang="typescript"
141// alchemy.run.ts
142export const Website = Hetzner.Website.Vite("Website", {
143+ env: {
144+ VITE_GREETING: "Hello from Alchemy!",
145+ },
146});
147```
148
149Vite inlines `VITE_*` into the client bundle at build time. Other
150keys reach the hosted Node process but not the browser.
151
152## Read the environment
153
154Client code reads inlined values from `import.meta.env`:
155
156```typescript
157// src/main.ts
158const greeting = import.meta.env.VITE_GREETING ?? "hello";
159```
160
161## Single-page vs multi-page
162
163Plain Vite apps are typically SPAs, so `assets.notFoundHandling`
164defaults to `"single-page-application"`: unmatched paths get
165`index.html` with status 200 so client-side routes deep-link. Use
166`"404-page"` for multi-page sites that ship a `404.html`:
167
168```typescript
169export const Website = Hetzner.Website.Vite("Docs", {
170 assets: { notFoundHandling: "404-page" },
171});
172```
173
174## Vite configuration
175
176`rootDir` is the project root (the directory containing
177`package.json`). Default `"."`.
178
179`vite.outDir` and `vite.base` are serializable overrides merged
180*over* the config file:
181
182```typescript
183export const Website = Hetzner.Website.Vite("Website", {
184 rootDir: "./app",
185 vite: { outDir: "build", base: "/docs/" },
186});
187```
188
189`outDir` is relative to `rootDir` and defaults to the file's
190`build.outDir` (Vite's default: `"dist"`). `base` is Vite's public
191base path.
192
193## Local development
194
195`alchemy dev` runs Vite's own dev server (native HMR) instead of
196deploying; `site.url` is the local address and no Server or Service
197is created. Wrap the site in `Alchemy.remote()` to deploy the real
198infrastructure even during dev.
199
200## Custom domain
201
202`domain` is a hostname string. It requires `zone` — an existing
203[`Hetzner.Zone`](/hetzner/networking/dns). Alchemy creates an A
204[`RecordSet`](/hetzner/networking/dns) pointing at the Server's
205public IPv4. `url` becomes `http://{domain}:3000`. Passing `domain`
206without `zone` fails. The Service does not terminate TLS:
207
208```typescript
209export const Dns = Hetzner.Zone("dns", { name: "example.com" });
210
211export const Website = Hetzner.Website.Vite("Website", {
212 domain: "app.example.com",
213 zone: Dns,
214});
215```
216
217## Where next
218
219- [`Hetzner.Website.Vite` API](/providers/hetzner/website/vite)
220- [Astro](/hetzner/frontend/astro),
221 [Next.js](/hetzner/frontend/nextjs),
222 [Nuxt](/hetzner/frontend/nuxt),
223 [SvelteKit](/hetzner/frontend/sveltekit),
224 [Waku](/hetzner/frontend/waku),
225 [Octane](/hetzner/frontend/octane) — SSR frameworks
226- [Servers](/hetzner/compute/servers),
227 [Services](/hetzner/compute/services),
228 [DNS](/hetzner/networking/dns)