---
title: "How to Run a Vite Dev Server in a Sandbox"
description: "Bake a Vite dev server into a VM snapshot, run it under systemd, and open it on a public domain."
url: "/docs/guides/run-vite-in-a-sandbox"
---

Build a snapshot with a Vite dev server already running, then boot a VM that serves it and route a public domain to it. The one Vite-specific step is telling the dev server to accept your domain.


## Install the SDK

```bash pnpm
pnpm add freestyle
```

```bash bun
bun add freestyle
```

```bash npm
npm install freestyle
```

```bash yarn
yarn add freestyle
```

Set your API key before calling the API:

```bash
export FREESTYLE_API_KEY="your-api-key"
```


## Build a Snapshot with the Vite Dev Server

Vite runs on Node, so start from a snapshot with Node installed — build one by following [How to Run Node.js in a Sandbox](https://www.freestyle.sh/docs/guides/run-nodejs-in-a-sandbox), which sets up Node 22 with nvm. Boot a builder from that `snapshotId`, scaffold a Vite app, and configure the dev server. Vite refuses requests whose `Host` header it does not recognize, so two settings matter: `server.host` makes it bind `0.0.0.0` (not just localhost), and `server.allowedHosts` has to list whatever host you serve the app on, the domain you map below. A leading-dot `.style.dev` entry matches any Freestyle subdomain (what this guide uses); if you map your own custom domain, list that instead — `app.example.com`, or `.example.com` for its subdomains. Run it under systemd, wait until it serves, then snapshot — a Freestyle snapshot captures the running process, so VMs booted from it come up serving.

```ts
import { freestyle } from "freestyle";

// nodeSnapshotId comes from the Node.js guide — a snapshot with Node 22 (via nvm) ready.
const { vm: builder } = await freestyle.vms.create({ snapshotId: nodeSnapshotId });

// Scaffold a Vite app and install its dependencies. `node` sources nvm each call.
const node = "export HOME=/root NVM_DIR=/opt/nvm && . $NVM_DIR/nvm.sh &&";
await builder.exec(`${node} mkdir -p /srv && cd /srv && npm create vite@latest app -- --template vanilla`);
await builder.exec(`${node} cd /srv/app && npm install`);
```

Write a Vite config that binds `0.0.0.0` and allows the public host:

```ts
await builder.fs.writeTextFile(
  "/srv/app/vite.config.js",
  `import { defineConfig } from "vite";

export default defineConfig({
  server: {
    host: true,
    port: 5173,
    // Accept the Host you serve on — match this to whatever you map. A leading-dot
    // ".style.dev" matches any Freestyle subdomain; use "app.example.com" (or
    // ".example.com" for its subdomains) for a custom domain, or one exact host.
    allowedHosts: [".style.dev"],
  },
});
`,
);
```

Run the dev server as a systemd unit. systemd is PID 1, so it keeps the process alive; the unit sources nvm so `npm` is on `PATH`:

```ts
await builder.fs.writeTextFile(
  "/etc/systemd/system/vite.service",
  `[Service]
Environment=HOME=/root
WorkingDirectory=/srv/app
ExecStart=/usr/bin/bash -lc 'export NVM_DIR=/opt/nvm; . "$NVM_DIR/nvm.sh"; exec npm run dev'
Restart=always

[Install]
WantedBy=multi-user.target
`,
);
await builder.exec("systemctl daemon-reload && systemctl enable --now vite");

// Wait until Vite serves on localhost, then snapshot the running dev server.
let code = "";
while (code !== "200") {
  await new Promise((r) => setTimeout(r, 2000));
  code = (
    await builder.exec("curl -s -o /dev/null -w '%{http_code}' http://localhost:5173/")
  ).stdout.trim();
}

const { snapshotId } = await builder.snapshot();
await builder.delete();
```


## Open It on a Domain

Create a VM from the snapshot — Vite is already serving — then route a domain to port `5173`. Pick your own unique `*.style.dev` subdomain; it needs no DNS or verification.

```ts
const { vm, vmId } = await freestyle.vms.create({ snapshotId, idleTimeoutSeconds: null });

const domain = `my-app-${crypto.randomUUID().slice(0, 8)}.style.dev`;
await freestyle.domains.mappings.create({ domain, vmId, vmPort: 5173 });

const res = await fetch(`https://${domain}`);
console.log(res.status); // 200
```

Open the printed URL in a browser and the Vite app loads, hot-reload included. Because `allowedHosts` covers the domain you mapped, the dev server accepts the request instead of returning a host-not-allowed error.


## Stream the Server's Logs

`vm.exec()` buffers a command and only returns once it finishes, so it can't show a long-running service's output as it happens. To watch the logs live, open a [PTY](https://www.freestyle.sh/docs/vms/pty) on the VM — a real terminal streamed over a WebSocket (server-side only, Node 22+) — and follow the unit's journal. `onData` delivers the bytes as they arrive:

```ts
const session = await vm.pty.open({
  cols: 120,
  rows: 30,
  onData: (bytes) => process.stdout.write(bytes), // live log lines
});

// Follow the service; new lines stream in until you detach.
session.write("journalctl -u vite -f\n");

// session.detach() drops your handle — the service keeps running in the VM.
```
