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@latestbun add freestyle@latestnpm install freestyle@latestyarn add freestyle@latest 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 freestyle = new Freestyle();
const { vm: builder } = await freestyle.vms.create({
// Required: a VM reaches nothing it has not been allowed to.
firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] }, slug: "vs-code-builder" });
// 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. Any unused subdomain of style.dev is yours to take, with no verification and no DNS records; to use a name you own, verify it and point its DNS at Freestyle first.
const { vm, vmId } = await freestyle.vms.create({
// Required: a VM reaches nothing it has not been allowed to.
firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] }, slug: "vs-code-server", snapshotId, idleTimeoutSeconds: null });
const domain = "my-editor.style.dev"; // any unused style.dev subdomain
await freestyle.tls.rules.create({
action: "allow",
domain,
source: { public: true },
destination: { vmId, port: 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.