How-to: expose a service with a preview URL
Give an HTTP server running inside a sandbox a public preview URL with the SDK.
At a glance
- Package:
@nodeops-createos/sandbox(npm) - Import:
import { createClient } from "@nodeops-createos/sandbox" - Base URL:
https://api.sb.createos.sh— override withCREATEOS_SANDBOX_BASE_URL - Auth: API key via the
apiKeyoption orCREATEOS_SANDBOX_API_KEY
Problem
You started an HTTP server inside the sandbox and want to reach it from outside — from a browser, a CI job, or your own code — without setting up SSH tunnels or port mappings.
Solution
The control plane provisions a public hostname for every sandbox created
with ingress_enabled: true. Any TCP server bound to 0.0.0.0 on an
arbitrary port inside the VM becomes reachable at a stable URL derived
from that hostname.
The canonical recipe:
- Create the sandbox with
ingress_enabled: true. - Start your server bound to
0.0.0.0:<port>(not127.0.0.1). - Call
waitForPortReady(port)to block until the listener is up. - Call
previewUrl(port)to get the public URL. fetchit, hand it to a browser, or pass it downstream.
TypeScript1import { CreateosSandboxClient } from "@nodeops-createos/sandbox";23const client = new CreateosSandboxClient({4 baseUrl: process.env.CREATEOS_SANDBOX_BASE_URL!,5 apiKey: process.env.CREATEOS_SANDBOX_API_KEY!,6});78const sandbox = await client.createSandbox({9 shape: "s-4vcpu-4gb",10 rootfs: "devbox:1",11 ingress_enabled: true, // provisions the public hostname12});1314// Get the URL before try so you can log it even if setup fails.15// Use scheme: "http" — see "Scheme: http vs https" below.16const url = sandbox.previewUrl(8080, { scheme: "http" });17console.log("preview URL:", url);1819try {20 // Start the server in the background.21 // IMPORTANT: bind to 0.0.0.0, not 127.0.0.1 — ingress forwards to eth0,22 // not loopback. A server bound to localhost is unreachable from outside.23 // nohup/setsid daemonises without systemd; redirect stdio or runCommand blocks.24 await sandbox.runCommand("sh", [25 "-c",26 "nohup setsid python3 -m http.server 8080 --bind 0.0.0.0 >/tmp/srv.log 2>&1 &",27 ]);2829 // Block until the port accepts connections inside the VM.30 // waitForPortReady probes /dev/tcp from inside — it confirms the listener31 // is up before ingress routing matters.32 await sandbox.waitForPortReady(8080, { timeoutMs: 15_000 });3334 // The port is bound. Fetch through the public ingress URL.35 const res = await fetch(url);36 console.log("HTTP", res.status, await res.text());37} finally {38 await sandbox.destroy();39}
Binding to 0.0.0.0 is required
Ingress routes traffic to the VM's eth0 interface, not loopback.
A server bound to 127.0.0.1 or localhost will not be reachable from
outside the VM, even though waitForPortReady (which probes from inside)
will succeed. Always pass --bind 0.0.0.0, --host 0.0.0.0, or the
equivalent flag for your server.
Backgrounding a long-running server
runCommand waits for the process to exit. To start a persistent server
you must detach it:
TypeScript1// Pattern: nohup + setsid + stdio redirect + trailing &2await sandbox.runCommand("sh", [3 "-c",4 "nohup setsid my-server --port 8080 >/tmp/server.log 2>&1 &",5]);
nohup— ignore SIGHUP so the process survives the shell dying.setsid— move into a new session (no controlling terminal).>/tmp/server.log 2>&1— redirect stdout/stderr; without this, the shell's stdio stays open andrunCommandblocks forever.&— background the process so the shell exits, returning control.
Scheme: http vs https
previewUrl defaults to https. Use { scheme: "http" } unless your
ingress wildcard domain has a provisioned TLS certificate:
TypeScript1// https (default) — only safe if TLS is provisioned for the hostname2const secureUrl = sandbox.previewUrl(8080);34// http — always works; use this when TLS is not yet provisioned5const plainUrl = sandbox.previewUrl(8080, { scheme: "http" });
An https preview against a missing or self-signed certificate will fail
in standard fetch clients and browsers. Prefer http unless you have
confirmed that TLS is available for the sandbox domain.
Enabling ingress after creation
If you created the sandbox without ingress_enabled, toggle it on with
setIngress:
TypeScript1await sandbox.setIngress(true); // PATCH; refreshes the handle in place2const url = sandbox.previewUrl(8080, { scheme: "http" });
setIngress returns this so it is chainable. The handle's cached
projection is updated with the new ingress_url_template.
Disable ingress
To revoke the public hostname while keeping the sandbox alive:
TypeScript1await sandbox.setIngress(false); // clears ingress_url_template on the handle
After this, previewUrl throws until ingress is re-enabled. Destroying
the sandbox also removes the hostname.
See also
Sandboxreference — fullsetIngress,waitForPortReady, andpreviewUrlsignatures.- Tutorial — end-to-end walkthrough including sandbox creation and cleanup.