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 freestylebun add freestylenpm install freestyleyarn 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.