NodeOps
UK

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 with CREATEOS_SANDBOX_BASE_URL
  • Auth: API key via the apiKey option or CREATEOS_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:

  1. Create the sandbox with ingress_enabled: true.
  2. Start your server bound to 0.0.0.0:<port> (not 127.0.0.1).
  3. Call waitForPortReady(port) to block until the listener is up.
  4. Call previewUrl(port) to get the public URL.
  5. fetch it, hand it to a browser, or pass it downstream.
TypeScript
1import { CreateosSandboxClient } from "@nodeops-createos/sandbox";
2
3const client = new CreateosSandboxClient({
4 baseUrl: process.env.CREATEOS_SANDBOX_BASE_URL!,
5 apiKey: process.env.CREATEOS_SANDBOX_API_KEY!,
6});
7
8const sandbox = await client.createSandbox({
9 shape: "s-4vcpu-4gb",
10 rootfs: "devbox:1",
11 ingress_enabled: true, // provisions the public hostname
12});
13
14// 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);
18
19try {
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 ]);
28
29 // Block until the port accepts connections inside the VM.
30 // waitForPortReady probes /dev/tcp from inside — it confirms the listener
31 // is up before ingress routing matters.
32 await sandbox.waitForPortReady(8080, { timeoutMs: 15_000 });
33
34 // 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:

TypeScript
1// 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 and runCommand blocks 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:

TypeScript
1// https (default) — only safe if TLS is provisioned for the hostname
2const secureUrl = sandbox.previewUrl(8080);
3
4// http — always works; use this when TLS is not yet provisioned
5const 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:

TypeScript
1await sandbox.setIngress(true); // PATCH; refreshes the handle in place
2const 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:

TypeScript
1await 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

  • Sandbox reference — full setIngress, waitForPortReady, and previewUrl signatures.
  • Tutorial — end-to-end walkthrough including sandbox creation and cleanup.

100,000+ Builders. One Workspace.

Get product updates, builder stories, and early access to features that help you ship faster.

CreateOS is a unified intelligent workspace where ideas move seamlessly from concept to live deployment, eliminating context-switching across tools, infrastructure, and workflows with the opportunity to monetize ideas immediately on the CreateOS Marketplace.