Vite
Hetzner.Website.Vite deploys a Vite project to
a Hetzner Cloud Server. It runs vite build and serves the output
from a systemd Service on port 3000.
Omit server and Alchemy creates a cpx12 / ubuntu-24.04
Server in fsn1. Live url is
http://{ipv4}:3000. Your vite.config.* loads natively — plugins
included. There is no TLS on the Service and no framework server
module.
React, Vue, and Solid client SPAs belong here, as do multi-page apps
that emit one HTML file per route. SSR frameworks that wrap Vite
deploy through their own composites —
Astro,
Nextjs,
Nuxt,
ReactRouter,
SolidStart,
SvelteKit,
TanStackStart,
Waku,
Octane.
Install
Section titled “Install”The build integration is not bundled with alchemy. Install
@alchemy.run/frontend-frameworks; the resource loads its /vite
and /vite/node exports from your project at deploy time. It is
only used at build time, so a dev dependency is enough:
bun add -d @alchemy.run/frontend-frameworksnpm install -D @alchemy.run/frontend-frameworkspnpm add -D @alchemy.run/frontend-frameworksyarn add -D @alchemy.run/frontend-frameworksConfigure Vite
Section titled “Configure Vite”Your vite.config.* file loads natively — React, Vue, or Solid
plugins, Tailwind, and any other non-serializable options work
exactly as they do outside Alchemy. Alchemy merges a programmatic
config over it via the vite prop — see
Vite configuration below.
import { defineConfig } from "vite";import react from "@vitejs/plugin-react";import tailwindcss from "@tailwindcss/vite";
export default defineConfig({ plugins: [react(), tailwindcss()],});Do not set a Vite SSR adapter. This composite never creates a framework server module — it only ships the client build.
Declare the Website
Section titled “Declare the Website”Declare the site as a module-level const (rather than inline in the Stack):
import * as Hetzner from "alchemy/Hetzner";
export const Website = Hetzner.Website.Vite("Website");Pass server to run the unit on a Server you already have —
several Services can share one machine; this site always listens on
port 3000:
export const Box = Hetzner.Server("Box", { serverType: "cpx12", image: "ubuntu-24.04", location: "fsn1",});
export const Website = Hetzner.Website.Vite("Website", { server: Box });Add it to the Stack
Section titled “Add it to the Stack”Yield the site from your Stack and return its URL:
import * as Alchemy from "alchemy";import * as Effect from "effect/Effect";
export default Alchemy.Stack( "MyViteSite", { providers: Hetzner.providers(), state: Alchemy.localState(), }, Effect.gen(function* () { const site = yield* Website; return { url: site.url }; }),);On deploy, url is http://{ipv4}:3000. server and service are
the backing resources; they are undefined under alchemy dev.
See
examples/hetzner-website-vite
for the checked-in example (API unit and static site on one
cpx12).
Add environment variables
Section titled “Add environment variables”env is process environment — set on process.env before
vite build / alchemy dev, and on the systemd unit at deploy. It
is not Worker bindings:
export const Website = Hetzner.Website.Vite("Website", { env: { VITE_GREETING: "Hello from Alchemy!", },});Vite inlines VITE_* into the client bundle at build time. Other
keys reach the hosted Node process but not the browser.
Read the environment
Section titled “Read the environment”Client code reads inlined values from import.meta.env:
const greeting = import.meta.env.VITE_GREETING ?? "hello";Single-page vs multi-page
Section titled “Single-page vs multi-page”Plain Vite apps are typically SPAs, so assets.notFoundHandling
defaults to "single-page-application": unmatched paths get
index.html with status 200 so client-side routes deep-link. Use
"404-page" for multi-page sites that ship a 404.html:
export const Website = Hetzner.Website.Vite("Docs", { assets: { notFoundHandling: "404-page" },});Vite configuration
Section titled “Vite configuration”rootDir is the project root (the directory containing
package.json). Default ".".
vite.outDir and vite.base are serializable overrides merged
over the config file:
export const Website = Hetzner.Website.Vite("Website", { rootDir: "./app", vite: { outDir: "build", base: "/docs/" },});outDir is relative to rootDir and defaults to the file’s
build.outDir (Vite’s default: "dist"). base is Vite’s public
base path.
Local development
Section titled “Local development”alchemy dev runs Vite’s own dev server (native HMR) instead of
deploying; site.url is the local address and no Server or Service
is created. Wrap the site in Alchemy.remote() to deploy the real
infrastructure even during dev.
Custom domain
Section titled “Custom domain”domain is a hostname string. It requires zone — an existing
Hetzner.Zone. Alchemy creates an A
RecordSet pointing at the Server’s
public IPv4. url becomes http://{domain}:3000. Passing domain
without zone fails. The Service does not terminate TLS:
export const Dns = Hetzner.Zone("dns", { name: "example.com" });
export const Website = Hetzner.Website.Vite("Website", { domain: "app.example.com", zone: Dns,});