Freestyle Docs

Freestyle / Guides

How to Run VS Code in the Browser in a Sandbox

Bake code-server — VS Code in the browser — into a VM snapshot, run it under systemd, and open the editor on a public domain.

Build a snapshot with 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

pnpm add freestyle
bun add freestyle
npm install freestyle
yarn add freestyle

Set your API key before calling the API:

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 to gate it behind one. A Freestyle snapshot captures the running process, so VMs booted from it come up already serving.

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.

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 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 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:

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.

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.

esc