# Hosting

> Serve any port of a sandbox on a stable HTTPS URL — private with a token, or public.

Any TCP service inside a sandbox can be published on a stable HTTPS address:

```text
https://<subdomain>-<port>.box.sandboxer.com
```

`<subdomain>` is the sandbox's three-word slug (for example `amber-fjord-lumen`). It stays the same across stop, resume and resize, so links you share keep working. TLS, HTTP/2 and WebSockets are handled for you.

## Host a port

Start your app on a known port, then host it.

```bash title="From your computer"
sbx exec sb_k3v9x2mq --detach "cd my-app && npm run dev -- --host 0.0.0.0 --port 3000"
sbx host sb_k3v9x2mq 3000 --title "App preview"
```

```bash title="Inside the sandbox"
npm run dev -- --host 0.0.0.0 --port 3000 &
host 3000 --title "App preview"
```

```bash title="curl"
curl -sS -X POST https://sandboxer.com/api/v1/sandboxes/sb_k3v9x2mq/host \
  -H "Authorization: Bearer $SANDBOXER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"port":3000,"title":"App preview"}'
```

```typescript title="TypeScript"
const hosted = await sbx.hostPort({ sandboxId: "sb_k3v9x2mq", port: 3000, title: "App preview" });
console.log(hosted.url);
```

Hosting is idempotent: hosting the same port again returns the same URL (and the same token).

## Private and public

| Mode | URL | Who can open it |
| - | - | - |
| Private (default) | `https://…-3000.box.sandboxer.com?_token=<secret>` | Anyone with the full URL. The first visit sets a cookie, so the app can drop the query parameter afterwards. |
| Public | `https://…-3000.box.sandboxer.com` | Anyone. |

```bash
sbx host sb_k3v9x2mq 3000 --public     # make it public (clears the token)
sbx host sb_k3v9x2mq 3000 --private    # back to private, with a fresh token
```

Treat a private URL like a password: it is redacted from our logs, and you should redact it from yours.

The cookie the first visit sets is `__sbx_auth`, holding the same token. Clients that drop query parameters or don't keep cookies, such as `git` over HTTPS or a Selenium RemoteWebDriver, can send it as a header on every request instead:

```bash
git -c http.extraHeader="Cookie: __sbx_auth=<secret>" clone https://…-3000.box.sandboxer.com/org/repo.git
curl -H "Cookie: __sbx_auth=<secret>" https://…-3000.box.sandboxer.com/api/health
```

The `__sbx_auth` cookie and `_token` are removed before the request reaches your app, so your app never sees the token.

If the sandbox has [paused itself for being idle](/docs/long-running#idle-auto-pause-and-wake-on-use), the next request to its URL wakes it: the request is held while it resumes (up to 45 seconds, then `503` with `Retry-After`) and then served. A private port wakes only for a request carrying its token; an expiring signed link (a desktop URL, or a [Daytona-API](/docs/daytona) signed preview URL) doesn't wake it. A sandbox you stopped, or one stopped by auto-stop or billing, stays stopped until you resume it; until then its URL returns an error page, and the sandbox itself is untouched.

## Apps that listen on localhost

The proxy reaches your app on the sandbox's own address, so an app bound to `0.0.0.0` just works. Many dev servers (Vite, Next.js in some modes) listen on `127.0.0.1` only. Tell `host` with `--localhost` and the sandbox relays the connection for you:

```bash
npm run dev -- --port 5173      # Vite defaults to localhost
host 5173 --localhost
```

- The relay forwards raw TCP, so WebSockets and hot module reload work.
- Don't combine `--localhost` with an app that binds `0.0.0.0` — the relay would hold the port.
- Each `host` call sets the mode again; hosting without `--localhost` turns the relay off.

Some dev servers reject unknown `Host` headers. For Vite add `.box.sandboxer.com` to `server.allowedHosts`; for webpack-dev-server set `allowedHosts: "all"`.

## List and remove

```bash
sbx host list sb_k3v9x2mq
sbx host hide sb_k3v9x2mq 3000
```

`hide` takes the URL down. The server keeps running in the sandbox, and private links work again if you host the port again.

```bash title="curl"
curl -sS https://sandboxer.com/api/v1/sandboxes/sb_k3v9x2mq/host \
  -H "Authorization: Bearer $SANDBOXER_API_KEY"

curl -sS -X DELETE "https://sandboxer.com/api/v1/sandboxes/sb_k3v9x2mq/host?port=3000" \
  -H "Authorization: Bearer $SANDBOXER_API_KEY"
```

The dashboard's sandbox page lists hosted ports with their URLs.

## Across stop, resume and fork

- **Stop/resume:** routes and tokens are kept. With memory-preserving resume your server is usually already running when the sandbox comes back, so the URL answers immediately.
- **Fork:** the fork has its own subdomain and no routes. Host the ports you need on the fork.
- **Rename:** `PATCH /sandboxes/{id}` with a new `subdomain` re-points every hosted URL to the new name without downtime. Old URLs stop resolving.

## How it works

```text
browser ──TLS──▶ box.sandboxer.com edge ──▶ sandbox node proxy ──▶ VM <guest-ip>:<port>
                                                             └─▶ (with --localhost) relay ──▶ 127.0.0.1:<port>
```

1. `host` registers a route for (subdomain, port) with the control plane.
2. The edge holds a wildcard certificate for `*.box.sandboxer.com` and forwards to the node that runs the sandbox.
3. The node's proxy checks the route (and token, for private routes) and connects to the VM.

Your app sees the request with its public `Host` header, plus `X-Forwarded-For` and `X-Real-IP` (the client's address), `X-Forwarded-Proto` (`https`) and `X-Forwarded-Host`. `X-Real-IP` is always set by the proxy, never taken from the client. The access token (`_token`, the `__sbx_auth` cookie and the preview-token header) is removed first.

The proxy only reaches ports you hosted. Nothing else on the sandbox is exposed by this mechanism. The VM also has a public IPv6 address. Its firewall (`ufw`) is closed except SSH; to serve a port directly on `http://[<ipv6>]:<port>`, run `sudo ufw allow <port>` inside the sandbox — see [Security](/docs/security). Hosted ports don't need this.

## Custom domains

Point a CNAME at the hosted hostname and terminate TLS yourself (for example with a proxy in front), or run Caddy inside the sandbox on its IPv6 address (after `sudo ufw allow 80,443/tcp`). First-class custom domains are on the roadmap.
