Sitelet https://github.com/NodeOps-app/createos-sandbox-sdk/commit/37a8e76afa6d7c06a40b3d7cde72500494111612
Skip to content

Commit 37a8e76

Browse files
authored
Merge pull request #7 from NodeOps-app/example/28-code-server-vscode
feat(examples): add 28-code-server-vscode example
2 parents cd547b2 + 20ab8f2 commit 37a8e76

4 files changed

Lines changed: 200 additions & 0 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export FCSPAWN_URL=https://fc-spawn.example.com
2+
export FC_API_KEY=your-api-key
3+
export FCSPAWN_GATEWAY=https://gateway.example.com
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# 28 — code-server (VS Code in Browser)
2+
3+
Run [code-server](https://github.com/coder/code-server) inside an FC microVM and expose a
4+
fully-functional VS Code IDE through the sandbox's public ingress URL.
5+
6+
## Run
7+
8+
```sh
9+
cp .env.example .env
10+
# fill in FCSPAWN_URL and FC_API_KEY
11+
bun index.ts
12+
```
13+
14+
`bun` auto-loads `.env` from the current directory.
15+
16+
## What it does
17+
18+
1. Creates a sandbox with `ingress_enabled: true` (`s-2vcpu-2gb`, `devbox:1`).
19+
2. Installs code-server via the official standalone installer (`--method=standalone`) — bundles its own Node runtime, no PATH conflicts.
20+
3. Daemonises `code-server --bind-addr 0.0.0.0:8080 --auth none` via `nohup setsid` (no systemd in `devbox:1`).
21+
4. Waits for port 8080 to accept connections with `waitForPortReady`.
22+
5. Polls `GET /healthz` through the public ingress URL until code-server returns `{"status":"success","data":{"up":true}}`.
23+
6. Prints the live VS Code URL.
24+
7. Destroys the sandbox in a `finally` block.
25+
26+
## FC primitives exercised
27+
28+
| Primitive | SDK call |
29+
| --- | --- |
30+
| Create sandbox with public ingress | `fc.createSandbox({ ingress_enabled: true })` |
31+
| Run commands inside the VM | `sandbox.runCommand("bash", ["-lc", ...])` |
32+
| Build the public preview URL | `sandbox.previewurl(/sitelet?url=https%3A%2F%2Fgithub.com%2FNodeOps-app%2Fcreateos-sandbox-sdk%2Fcommit%2Fport)` |
33+
| Block until the server listens | `sandbox.waitForPortReady(port)` |
34+
| Tear the sandbox down | `sandbox.destroy()` |
35+
36+
## Versions captured at build time
37+
38+
See `versions.txt`.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// 28 — code-server (VS Code in Browser) inside an FC sandbox.
2+
// Installs code-server inside an FC microVM, binds it on 0.0.0.0:8080
3+
// with auth disabled, exposes it through the public ingress URL, and
4+
// verifies the workbench is live by hitting GET /healthz.
5+
6+
import type { Sandbox } from "fc-sandbox-sdk";
7+
import { FcClient } from "fc-sandbox-sdk";
8+
9+
const SHAPE = "s-2vcpu-2gb";
10+
const ROOTFS = "devbox:1";
11+
const PORT = 8080;
12+
13+
// exactOptionalPropertyTypes: narrow env vars before passing to FcClient.
14+
const baseUrl = process.env.FCSPAWN_URL;
15+
const apiKey = process.env.FC_API_KEY;
16+
if (!baseUrl || !apiKey) {
17+
throw new Error("set FCSPAWN_URL and FC_API_KEY (see .env.example)");
18+
}
19+
20+
const fc = new FcClient({ baseUrl, apiKey });
21+
22+
async function sh(sb: Sandbox, label: string, script: string, timeoutMs = 120_000) {
23+
const { result, exec_ms } = await sb.runCommand("bash", ["-lc", script], { timeoutMs });
24+
if (result.exit_code !== 0) {
25+
console.log(`[${label}] exit=${result.exit_code} (${exec_ms} ms)`);
26+
if (result.stdout) console.log(" stdout:", result.stdout.slice(-2000));
27+
if (result.stderr) console.log(" stderr:", result.stderr.slice(-2000));
28+
throw new Error(`${label} failed (exit ${result.exit_code})`);
29+
}
30+
return result.stdout;
31+
}
32+
33+
console.log(`[1/6] creating sandbox (shape=${SHAPE}, rootfs=${ROOTFS}, ingress on)...`);
34+
const sandbox = await fc.createSandbox({
35+
shape: SHAPE,
36+
rootfs: ROOTFS,
37+
ingress_enabled: true,
38+
});
39+
console.log(` sandbox: ${sandbox.id} ip: ${sandbox.ip}`);
40+
41+
// Build the public ingress URL. Template yields https://<ulid>-<port>.<region>.<domain>.
42+
// Downgrade to http:// — TLS wildcard cert is not yet provisioned; http:// is forward-compatible.
43+
const previewUrl = sandbox.previewUrl(PORT).replace(/^https:/, "http:");
44+
console.log(` preview URL: ${previewUrl}`);
45+
46+
try {
47+
// Ensure curl is available (devbox:1 is Debian-based).
48+
console.log("[2/6] ensuring curl is available...");
49+
await sh(sandbox, "curl-check", "curl --version >/dev/null || apt-get install -y curl", 60_000);
50+
51+
// Standalone install bundles the Node runtime — predictable, no PATH conflicts.
52+
// ~120-200 MB download; 300 s budget is sufficient on the FC egress link.
53+
console.log("[3/6] installing code-server (standalone, ~100-200 MB)...");
54+
await sh(
55+
sandbox,
56+
"install",
57+
"curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone",
58+
300_000,
59+
);
60+
61+
// Extract the semver from `code-server --version` output (first line is the bare version).
62+
const codeServerVer =
63+
(await sh(sandbox, "version", "code-server --version 2>&1"))
64+
.split("\n")
65+
.map((l) => l.trim())
66+
.find((l) => /^\d+\.\d+/.test(l)) ?? "unknown";
67+
console.log(` code-server ${codeServerVer}`);
68+
69+
// Daemonise with nohup/setsid — devbox:1 has no systemd.
70+
// `;` before nohup so the chain does not hold the /exec stdout pipe open.
71+
// --auth none: no password prompt for a short-lived demo sandbox.
72+
// --bind-addr 0.0.0.0:PORT: required for ingress (127.0.0.1 is not reachable from outside the VM).
73+
console.log(`[4/6] daemonising code-server on port ${PORT}...`);
74+
await sh(
75+
sandbox,
76+
"boot",
77+
`rm -f /tmp/code-server.log ; ` +
78+
`nohup setsid code-server --bind-addr 0.0.0.0:${PORT} --auth none ` +
79+
`>/tmp/code-server.log 2>&1 </dev/null & sleep 1; echo launched`,
80+
);
81+
82+
console.log(`[5/6] waiting for code-server to bind port ${PORT}...`);
83+
await sandbox.waitForPortReady(PORT, { timeoutMs: 60_000 });
84+
console.log(" port is accepting connections");
85+
86+
// Poll GET /healthz through the ingress URL until code-server responds.
87+
// /healthz is an auth-exempt route that returns JSON {"status":"success","data":{"up":true}}.
88+
console.log(`[6/6] polling ${previewUrl}/healthz for a live response...`);
89+
const deadline = Date.now() + 90_000;
90+
let healthBody = "";
91+
let healthStatus = 0;
92+
let healthy = false;
93+
while (Date.now() < deadline) {
94+
try {
95+
const res = await fetch(`${previewUrl}/healthz`, { signal: AbortSignal.timeout(10_000) });
96+
healthStatus = res.status;
97+
healthBody = await res.text();
98+
if (res.ok) {
99+
let json: { status?: string; data?: { up?: boolean } } = {};
100+
try {
101+
json = JSON.parse(healthBody);
102+
} catch {
103+
// not JSON yet — keep polling
104+
}
105+
if (json.data?.up === true) {
106+
healthy = true;
107+
break;
108+
}
109+
}
110+
} catch {
111+
// ingress propagation still in flight — keep polling
112+
}
113+
await new Promise((r) => setTimeout(r, 2_000));
114+
}
115+
116+
if (!healthy) {
117+
// Fetch the log for diagnostics before throwing.
118+
const log = await sh(sandbox, "log", "tail -40 /tmp/code-server.log").catch(
119+
() => "(unavailable)",
120+
);
121+
throw new Error(
122+
`GET /healthz never returned a live response (last HTTP ${healthStatus}).\n` +
123+
`Body: ${healthBody.slice(0, 300)}\ncode-server log:\n${log}`,
124+
);
125+
}
126+
127+
let healthJson: { status?: string; data?: { up?: boolean } } = {};
128+
try {
129+
healthJson = JSON.parse(healthBody);
130+
} catch {
131+
// non-fatal — healthy flag already confirmed data.up === true
132+
}
133+
134+
console.log(`\n── GET /healthz (HTTP ${healthStatus}) ────────────────────────────────`);
135+
console.log(" ", JSON.stringify(healthJson));
136+
137+
console.log(`\ncode-server is live at: ${previewUrl}`);
138+
console.log(`(open this URL in a browser to use VS Code in the sandbox)`);
139+
140+
// Extract region from preview URL hostname for versions.txt output.
141+
const hostname = new URL(previewUrl).hostname;
142+
const region = hostname.split("-").slice(-1)[0]?.split(".").slice(1, -2).join(".") ?? "eu";
143+
console.log(`\n── versions (for versions.txt) ─────────────────────────────────`);
144+
console.log(`fc control plane: ${baseUrl} (region ${region})`);
145+
console.log(`code-server: ${codeServerVer}`);
146+
} finally {
147+
console.log("\ncleanup...");
148+
await sandbox.destroy().catch((err) => {
149+
console.error("destroy failed:", err instanceof Error ? err.message : String(err));
150+
});
151+
console.log(`destroyed sandbox: ${sandbox.id}`);
152+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
fc-sandbox-sdk: 0.3.0
2+
fc control plane: https://fc-spawn.bhautik.in (region eu)
3+
captured: 2026-05-29
4+
rootfs: devbox:1
5+
shape: s-2vcpu-2gb
6+
code-server: 4.122.0 (with VS Code 1.122.0)
7+
verified end-to-end: yes

0 commit comments

Comments
 (0)