1---
2title: SolidStart
3description: Deploy a SolidStart app to Hetzner with Hetzner.Website.SolidStart — nitro's node preset as a systemd unit on port 3000, prerendered pages baked into the unit, and SolidStart's own Vite dev server under alchemy dev.
4sidebar:
5 order: 10.5
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12`Hetzner.Website.SolidStart` deploys a
13[SolidStart](https://start.solidjs.com) app to a Hetzner Cloud Server.
14It runs your project's own `vite build` and appends nitro's `node`
15preset: the SSR server runs as a systemd
16[`Hetzner.Service`](/hetzner/compute/services) on port 3000, and
17client assets plus prerendered pages are baked into the unit and
18served first. Omit `server` and Alchemy creates a `cpx12` Ubuntu
1924.04 box in `fsn1`. The live URL is `http://{ipv4}:3000` — the
20Service has no TLS. No adapter.
21
22:::note[Verified version line]
23This page tracks `@solidjs/start` **2.x** with
24`@solidjs/vite-plugin-nitro-2` — the Vite-native line where
25`solidStart()` is a plain Vite plugin and nitro is a second plugin.
26:::
27
28## Install
29
30The build integration is not bundled with alchemy. Install
31`@alchemy.run/frontend-frameworks`; the resource loads its
32`/solidstart` and `/solidstart/node` exports from your project at
33deploy time. It is only used at build time, so a dev dependency is
34enough:
35
36<Tabs syncKey="pkgManager">
37 <TabItem label="bun" icon="bun">
38 <Code code={`bun add -d ${devDeps}`} lang="sh" />
39 </TabItem>
40 <TabItem label="npm" icon="npm">
41 <Code code={`npm install -D ${devDeps}`} lang="sh" />
42 </TabItem>
43 <TabItem label="pnpm" icon="pnpm">
44 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
45 </TabItem>
46 <TabItem label="yarn" icon="seti:yarn">
47 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
48 </TabItem>
49</Tabs>
50
51`@solidjs/vite-plugin-nitro-2` is the server half of a SolidStart
52build and must be a project dependency too:
53
54<Tabs syncKey="pkgManager">
55 <TabItem label="bun" icon="bun">
56 <Code code="bun add @solidjs/vite-plugin-nitro-2" lang="sh" />
57 </TabItem>
58 <TabItem label="npm" icon="npm">
59 <Code code="npm install @solidjs/vite-plugin-nitro-2" lang="sh" />
60 </TabItem>
61 <TabItem label="pnpm" icon="pnpm">
62 <Code code="pnpm add @solidjs/vite-plugin-nitro-2" lang="sh" />
63 </TabItem>
64 <TabItem label="yarn" icon="seti:yarn">
65 <Code code="yarn add @solidjs/vite-plugin-nitro-2" lang="sh" />
66 </TabItem>
67</Tabs>
68
69## Configure Vite
70
71Your `vite.config.ts` is the SolidStart plugin — no adapter, no
72nitro wiring. Vite plugins like Tailwind go in `plugins` as usual:
73
74```typescript
75// vite.config.ts
76import { solidStart } from "@solidjs/start/config";
77import tailwindcss from "@tailwindcss/vite";
78import { defineConfig } from "vite";
79
80export default defineConfig({
81 plugins: [solidStart(), tailwindcss()],
82});
83```
84
85Alchemy appends its own `nitroV2Plugin()` instance at build time,
86carrying nitro's `node` preset. Don't register `nitroV2Plugin()`
87yourself — the appended instance would discard its options, so the
88build fails with an actionable error instead. Pass nitro options
89through the resource's `nitro` prop; see [Prerendering](#prerendering)
90below.
91
92## Declare the Website
93
94Declare the site as a module-level const (rather than inline in the
95Stack):
96
97```typescript
98// alchemy.run.ts
99import * as Hetzner from "alchemy/Hetzner";
100
101export const Website = Hetzner.Website.SolidStart("Website");
102```
103
104Pass `server` to host the site on an existing
105[`Hetzner.Server`](/hetzner/compute/servers) — several sites and
106APIs can share one box. `rootDir` is the project root — the directory
107holding `package.json` and `vite.config.*`. It defaults to `"."`, so
108set it only when the app does not sit next to `alchemy.run.ts`:
109
110```typescript
111export const Website = Hetzner.Website.SolidStart("Website", {
112 rootDir: "./app",
113});
114```
115
116## Add it to the Stack
117
118Yield the site from your Stack and return its URL:
119
120```typescript
121// alchemy.run.ts
122import * as Alchemy from "alchemy";
123import * as Effect from "effect/Effect";
124
125export default Alchemy.Stack(
126 "MySolidStartSite",
127 {
128 providers: Hetzner.providers(),
129 state: Alchemy.localState(),
130 },
131 Effect.gen(function* () {
132 const site = yield* Website;
133 return { url: site.url };
134 }),
135);
136```
137
138The live `url` is `http://{ipv4}:3000`. `site.server` and
139`site.service` are the Hetzner resources underneath — `undefined`
140during `alchemy dev`.
141
142See the [SolidStart API reference](/providers/hetzner/website/solidstart)
143for every prop and attribute.
144
145## Add environment variables
146
147Process environment is a top-level `env` map — plain strings, or
148`Redacted` values:
149
150```diff lang="typescript"
151// alchemy.run.ts
152export const Website = Hetzner.Website.SolidStart("Website", {
153+ env: {
154+ API_BASE: "https://api.example.com",
155+ },
156});
157```
158
159The values are copied onto `process.env` before the build and the
160dev server, and onto the systemd unit at deploy time, so server
161code reads the same keys in both modes. Client inlining is whatever
162Vite does (`VITE_` prefixes).
163
164An output from another resource only exists once that resource is
165yielded, so declare the site inside the Stack generator instead:
166
167```typescript
168// alchemy.run.ts
169Effect.gen(function* () {
170 const api = yield* Api;
171
172 const site = yield* Hetzner.Website.SolidStart("Website", {
173 env: { API_BASE: api.url },
174 });
175
176 return { url: site.url };
177});
178```
179
180## Read the environment in server code
181
182The SolidStart server is a plain Node process, so API routes,
183server functions, and SSR read the environment from `process.env`:
184
185```typescript
186// src/routes/api/hello.ts
187export function GET() {
188 return new Response(process.env.API_BASE ?? "unset");
189}
190```
191
192Values prefixed `VITE_` are inlined into the client bundle at build
193time instead, as `import.meta.env.VITE_*` — set those in the shell
194that runs `alchemy deploy`, not in `env`.
195
196## Prerendering
197
198Nitro renders the routes you list at build time into `.output/public`.
199They are baked into the unit and served as static files with no
200handler invocation:
201
202```typescript
203export const Website = Hetzner.Website.SolidStart("Website", {
204 nitro: { prerender: { routes: ["/", "/about"] } },
205});
206```
207
208`prerender` takes the routes to render (`routes`) and can crawl links
209from prerendered pages to prerender them too (`crawlLinks`). The
210`nitro` prop is the home for all nitro options (route rules, storage,
211...) — the integration owns the plugin instance, so they cannot live
212in `vite.config.ts`. Values must be JSON-serializable; `preset` is
213owned by the Node deploy target, and a foreign preset is a hard error.
214
215## Local dev
216
217```sh
218bun alchemy dev
219```
220
221`alchemy dev` runs SolidStart's own Vite dev server (native HMR)
222instead of deploying; `site.url` is the local address and no Hetzner
223resources are created. Nitro plays no part in dev. Wrap the site in
224`Alchemy.remote()` to deploy the live Server and Service even during
225dev:
226
227```typescript
228export const Website = Hetzner.Website.SolidStart("Website").pipe(
229 Alchemy.remote(),
230);
231```
232
233SolidStart's plugin resolves its app root and route directory from
234the process working directory, so one `alchemy dev` process serves
235one SolidStart app at a time.
236
237:::caution[Run `alchemy dev` under Node]
238SolidStart 2's dev server answers requests through `srvx/node`, which
239returns the string `[object Object]` instead of the rendered document
240when it runs under Bun. Production builds are unaffected — only the
241dev server is. Until that upstream gap closes, run `alchemy dev` with
242Node (`npx alchemy dev`) for SolidStart projects.
243:::
244
245## Custom domain
246
247```typescript
248const zone = yield* Hetzner.Zone("dns", { name: "example.com" });
249const site = yield* Hetzner.Website.SolidStart("Web", {
250 domain: "app.example.com",
251 zone,
252});
253```
254
255`domain` requires an existing [`Hetzner.Zone`](/hetzner/networking/dns)
256— the Website does not create one. It adds an A record pointing at
257the Server's public IPv4. `url` becomes `http://app.example.com:3000`.
258There is no TLS on the Service.
259
260## Where next
261
262- [SolidStart API reference](/providers/hetzner/website/solidstart)
263- [Servers](/hetzner/compute/servers) and
264 [Services](/hetzner/compute/services)
265- [DNS](/hetzner/networking/dns)