---
title: "How to Run VS Code in the Browser in a Sandbox"
description: "Bake code-server — VS Code in the browser — into a VM snapshot, run it under systemd, and open the editor on a public domain."
url: "/docs/guides/run-vs-code-in-a-sandbox"
---

Build a snapshot with [code-server](https://github.com/coder/code-server) — full VS Code running in the browser — already serving, then boot a VM that comes up ready and route a public domain to it. Open the URL and you get a real editor, terminal, and extensions, all running inside the sandbox.


## 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 code-server Running

Install code-server with its official script, run it as a systemd service bound to `0.0.0.0`, and wait until it serves before snapshotting. The install script writes to `~/.cache` and `~/.config`, but the exec shell has no `HOME`, so set `HOME=/root` first. `--auth none` starts the editor open, with no login screen — see [Add a Password](#add-a-password) to gate it behind one. A Freestyle snapshot captures the running process, so VMs booted from it come up already serving.

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

const { vm: builder } = await freestyle.vms.create();

// The install script needs HOME; the exec shell has none, so set it.
await builder.exec(
  "export HOME=/root && curl -fsSL https://code-server.dev/install.sh | sh",
);

// Run code-server as a systemd unit. systemd is PID 1, so it supervises the editor.
await builder.fs.writeTextFile(
  "/etc/systemd/system/code-server.service",
  `[Service]
Environment=HOME=/root
ExecStart=/usr/bin/code-server --bind-addr 0.0.0.0:8080 --auth none --disable-telemetry
WorkingDirectory=/root
Restart=always
[Install]
WantedBy=multi-user.target
`,
);
await builder.exec("systemctl daemon-reload && systemctl enable --now code-server");

// Wait until code-server answers its health check, then snapshot the running editor.
let code = "";
while (code !== "200") {
  await new Promise((r) => setTimeout(r, 1500));
  code = (
    await builder.exec("curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/healthz")
  ).stdout.trim();
}

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


## Open VS Code on a Domain

Create a VM from the snapshot — code-server is already running — then route a domain to port 8080. Pick your own unique `*.style.dev` subdomain; it needs no DNS or verification and gets HTTPS automatically.

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

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

console.log(`https://${domain}`);
```

Open that URL in a browser and VS Code loads — editor, integrated terminal, and extensions, all running in the sandbox. Anyone with the link gets straight in; [add a password](#add-a-password) for anything you do not want public.


## 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 code-server -f\n");

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


## Add a Password

The build above runs code-server open, so the domain drops visitors straight into the editor. To require a login instead, set a `PASSWORD` in the unit and switch `--auth none` to `--auth password`. Make this change in the **build step** and re-snapshot — the credential is baked into the snapshot, so every VM booted from it comes up gated.

```ts {5-6}
await builder.fs.writeTextFile(
  "/etc/systemd/system/code-server.service",
  `[Service]
Environment=HOME=/root
Environment=PASSWORD=change-me-to-a-secret
ExecStart=/usr/bin/code-server --bind-addr 0.0.0.0:8080 --auth password --disable-telemetry
WorkingDirectory=/root
Restart=always
[Install]
WantedBy=multi-user.target
`,
);
```

Now the domain serves a login screen first; enter the `PASSWORD` from the unit to reach the editor. Use a long, random value — `crypto.randomUUID()` works — for anything you do not want public. The `/healthz` endpoint stays open with or without auth, so the readiness loop in the build step is unchanged.
