# How Do I Give a Port in a Cloud Sandbox an HTTPS URL?

> Start your server on a port, then run sbx host <id> <port>. You get https://<subdomain>-<port>.box.sandboxer.com with TLS and WebSockets. It is private by default: requests without the URL's _token get 401. Add --public for an open URL, or --localhost for apps bound to 127.0.0.1.

Source: https://sandboxer.com/guides/give-a-sandbox-port-an-https-url. Updated 2026-10-27.

**Short answer:** Start your server on a port, then run sbx host <id> <port>. You get https://<subdomain>-<port>.box.sandboxer.com with TLS and WebSockets. It is private by default: requests without the URL's _token get 401. Add --public for an open URL, or --localhost for apps bound to 127.0.0.1.

## At a glance

| You want | Run | What a request without the token gets |
| - | - | - |
| A link only you and the people you send it to can open | `sbx host $id 8000` | `401 Access token required` |
| A URL anyone can open | `sbx host $id 8000 --public` | `200` |
| A dev server that listens on 127.0.0.1 only | `sbx host $id 5173 --localhost` | Depends on `--public` |
| To see what's hosted | `sbx host list $id` | |
| To take a URL down | `sbx host hide $id 8000` | `404 Port not hosted` |

Everything below was run on a `small` sandbox on 2026-10-27. Tokens are replaced with `<token>`.

## Host a port

Start a server on a known port, bound to `0.0.0.0`, then host it.

```bash
id=sb_m0lu5n3z2r    # your sandbox id from `sbx new`

sbx exec $id --detach "mkdir -p site && cd site && echo hello from the sandbox > index.html && python3 -m http.server 8000"
sbx host $id 8000 --title "Demo"
```

```text
https://frank-ready-marsh-8000.box.sandboxer.com?_token=<token>
Private URL: keep the _token secret. `sbx host sb_m0lu5n3z2r 8000 --public` makes it ungated.
```

The hostname is the sandbox's subdomain plus the port. Running `sbx host` again for the same port returned the same URL and the same token. Inside the sandbox, the shorter `host 8000` does the same thing for the sandbox it runs in.

## What a private URL does without its token

Without the token, the proxy answers itself. Your server never sees the request.

```bash
curl -si https://frank-ready-marsh-8000.box.sandboxer.com/ | head -1
```

```text
HTTP/2 401
```

The body is a page titled "Access token required". A wrong token also gets `401`.

With the token, the first response is a redirect that sets a cookie and drops the token from the address bar:

```bash
curl -si "https://frank-ready-marsh-8000.box.sandboxer.com?_token=<token>"
```

```text
HTTP/2 302
location: /
set-cookie: __sbx_auth=<cookie>; Path=/; Max-Age=2592000; HttpOnly; Secure; SameSite=Lax
```

After that, the cookie alone is enough, so a browser can follow links and load assets normally. The cookie lasts 30 days. Treat the full URL and the cookie like a password.

## Make it public, or private again

```bash
sbx host $id 8000 --public
curl -s -w "\n%{http_code}\n" https://frank-ready-marsh-8000.box.sandboxer.com/
```

```text
hello from the sandbox

200
```

`sbx host $id 8000 --private` gates the URL again with a new token. Switching between public and private mints a new token, so links and cookies from before stop working: in our re-test on 2026-10-27, the old private link answered `401` after a public-then-private switch, and the new one worked. (An earlier run that night, before a fix shipped, kept the same token.) Running `sbx host` again with the same access keeps the token, so to cut someone off, switch the port to public and back to private, or stop serving it with `sbx host hide`.

## Apps that listen on localhost only

Many dev servers (Vite, for example) bind to `127.0.0.1`. The proxy connects to the sandbox's own address, so a normal `host` can't reach them:

```bash
sbx exec $id --detach "mkdir -p lh && cd lh && echo only on loopback > index.html && python3 -m http.server 5173 --bind 127.0.0.1"
sbx host $id 5173 --public
curl -s -o /dev/null -w "%{http_code}\n" https://frank-ready-marsh-5173.box.sandboxer.com/
```

```text
502
```

The `502` page says "Nothing is answering on port 5173". Host it with `--localhost` and the sandbox relays the connection to the loopback address:

```bash
sbx host $id 5173 --public --localhost
curl -s https://frank-ready-marsh-5173.box.sandboxer.com/
```

```text
only on loopback
```

Each `host` call sets the mode again, so hosting without `--localhost` turns the relay off. Don't use `--localhost` for an app that binds `0.0.0.0`. Some dev servers also reject unknown `Host` headers: for Vite, add `.box.sandboxer.com` to `server.allowedHosts`.

## WebSockets

We ran a small WebSocket echo server on port 8765 and connected with Node 24's built-in `WebSocket`. On a public port it works as is:

```text
wss://frank-ready-marsh-8765.box.sandboxer.com/   ->   echo: hello
```

On a private port, how you pass the token matters:

| Handshake carries | Result |
| - | - |
| Nothing | `401` |
| `?_token=<token>` | `302` redirect, which WebSocket clients treat as a failed handshake |
| The `__sbx_auth` cookie | `101 Switching Protocols`, works |

In a browser this works without extra steps: opening the token link sets the cookie, and the page's own WebSocket connections send it. A script has to trade the token for the cookie first:

```javascript
// node ws-cookie.mjs "<private hosted URL with ?_token=…>"
const url = new URL(process.argv[2]);
const res = await fetch(url, { redirect: "manual" });
const cookie = res.headers.get("set-cookie").split(";")[0];
url.searchParams.delete("_token");
url.protocol = "wss:";
const ws = new WebSocket(url, { headers: { Cookie: cookie } });
ws.onopen = () => ws.send("hello");
ws.onmessage = (e) => { console.log(e.data); process.exit(0); };
```

```text
echo: hello
```

## List and take down

```bash
sbx host list $id
```

```text
PORT  ACCESS              URL                                                                TITLE
5173  public (localhost)  https://frank-ready-marsh-5173.box.sandboxer.com
8000  private             https://frank-ready-marsh-8000.box.sandboxer.com?_token=%E2%80%A6  Demo
```

The list hides tokens. `sbx host hide $id 5173` removes the route and turns off the relay. The URL then returns `404` ("Port not hosted"), but the server keeps running inside the sandbox. A port you never hosted returns the same `404`. Only hosted ports are reachable this way.

## After stop, resume and fork

- **Stopped:** while the sandbox was stopped, its URLs returned `404` ("No sandbox uses this address").
- **Resumed:** `sbx resume` brought back every route and token. In our test the resume restored memory, so the same `http.server` processes (same PIDs) answered at once: the public URL returned `200`, the old private link returned its `302`, and a 30-day cookie from before the stop still worked. Memory is restored only when the resume lands on a machine with the same CPU type. Otherwise the sandbox boots from disk and shows `memoryRestored: false`: routes and tokens are kept, but you must start your servers again (or run them as enabled systemd services).
- **Forked:** `sbx fork` gave a new subdomain (`woody-merry-quarry`), no hosted ports and no running servers. Start your server and host it again on the fork.

## Sandboxer hosting or a tunnel?

Tunnels like ngrok and Cloudflare Tunnel solve a related problem: they publish a port on a machine you already run, through an agent process that connects out. The table uses only what each vendor's own docs say.

| | `sbx host` | ngrok | Cloudflare quick tunnel | Cloudflare named tunnel |
| - | - | - | - | - |
| Command | `sbx host <id> <port>` | `ngrok http 8080` ([quickstart](https://ngrok.com/docs/share-localhost/quickstart)) | `cloudflared tunnel --url http://localhost:8080` ([docs](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/)) | Configured in the dashboard or API ([docs](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel/)) |
| Extra process to run | None | The ngrok agent | `cloudflared` | `cloudflared` |
| Account | Your Sandboxer account | An ngrok account and authtoken | Not needed | Cloudflare account, with a website added to Cloudflare |
| Hostname | `<subdomain>-<port>.box.sandboxer.com`, the same across stop and resume | A dev domain chosen automatically for your account | A random `trycloudflare.com` name that changes on each start | Your own hostname |
| Access control | Private token by default, or `--public` | OAuth, basic auth and more through Traffic Policy ([auth](https://ngrok.com/docs/share-localhost/auth)) | None listed | Open to anyone unless you add a Cloudflare Access application |
| Stated limits | Only hosted ports are reachable | | 200 in-flight requests, no Server-Sent Events, no uptime guarantee, for testing | |

Use `sbx host` when the server already runs in a Sandboxer sandbox. Use a tunnel when the server runs somewhere else, such as your laptop, or when you need your own domain today. Sandboxer doesn't offer first-class custom domains yet: you can point a CNAME at the hosted name and terminate TLS yourself, as described in [Hosting](/docs/hosting).

## FAQ

### Is the URL public by default?

No. A new hosted port is private. Requests without the URL's `_token` (or the cookie it sets) get `401`, and they never reach your server. Add `--public` for an open URL.

### Why do I get 502 "Nothing is answering"?

Either the server isn't running, or it listens on `127.0.0.1` only. Bind it to `0.0.0.0`, or host the port with `--localhost`.

### Do WebSockets and hot module reload work?

Yes. On a public port they work directly. On a private port the handshake needs the `__sbx_auth` cookie, which a browser already has after opening the token link.

### Does the URL change when the sandbox stops and resumes?

No. Routes and tokens are kept, and the subdomain stays the same. A fork gets a new subdomain with no hosted ports.

### Do I need to open the firewall for a hosted port?

No. Hosted traffic comes through the HTTPS proxy. You only need `sudo ufw allow <port>` to serve directly on the sandbox's IPv6 address.

## Related

- [Hosting](/docs/hosting)
- [Long-running tasks](/docs/long-running)
- [Pause and resume a sandbox with its memory](/guides/pause-and-resume-a-sandbox-with-memory)
- [Security](/docs/security)
