> ## Documentation Index
> Fetch the complete documentation index at: https://docs.boat.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Host with Boat

> Run a service inside a sandbox and expose it on a stable public HTTPS URL.

<video controls playsInline preload="metadata" poster="/videos/hosting.jpg" src="https://mintcdn.com/ariana-1e97e6c3/KkeC9MhSefNUGJb6/videos/hosting.mp4?fit=max&auto=format&n=KkeC9MhSefNUGJb6&q=85&s=c720e3be22d2e947e860fce08899fc1c" className="w-full aspect-video rounded-xl" data-path="videos/hosting.mp4" />

## Quick path

1. Enter or target a sandbox. In integrations, use the command endpoint to run the same setup inside the Sandbox:

<CodeGroup>
  ```bash CLI theme={null}
  boat ssh bx_f7k2q9hd
  npm run dev -- --host 0.0.0.0 --port 3000
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/commands" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"npm run dev -- --host 0.0.0.0 --port 3000","cwd":"my-repo"}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.command({
    sandboxId: "bx_f7k2q9hd",
    command: "npm run dev -- --host 0.0.0.0 --port 3000", cwd: "my-repo",
  });
  ```

  ```python Python theme={null}
  sandbox.command("bx_f7k2q9hd", CommandRequest(
      command="npm run dev -- --host 0.0.0.0 --port 3000",
      cwd="my-repo",
  ))
  ```
</CodeGroup>

2. Start your app on an explicit port.

<Warning>
  Your app must bind to `0.0.0.0`. If it only listens on `localhost` or `127.0.0.1`,
  the hosted HTTPS URL will not be able to reach it.
</Warning>

3. Inside the sandbox, run `host` to start hosting your app. Programmatic integrations can execute the same command in the Sandbox:

<CodeGroup>
  ```bash CLI theme={null}
  host 3000 --title "App preview"
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/commands" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"host 3000 --title \"App preview\""}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.command({
    sandboxId: "bx_f7k2q9hd",
    command: 'host 3000 --title "App preview"',
  });
  ```

  ```python Python theme={null}
  sandbox.command("bx_f7k2q9hd", CommandRequest(command='host 3000 --title "App preview"'))
  ```
</CodeGroup>

4. Open the public HTTPS URL printed by `host`:

```text theme={null}
https://<sandbox-subdomain>-3000.on.boat.dev
```

If the port is protected, the usable URL includes a `_token` query parameter:

```text theme={null}
https://<sandbox-subdomain>-3000.on.boat.dev?_token=<access-token>
```

<Info>
  A fresh `host <port>` URL is protected by default and includes `_token`. Pass
  `--public` only when you want to clear the access token and return an ungated URL.
</Info>

<Info>
  If you want the process to survive after your SSH command exits, start it as a
  detached process before hosting the port.
</Info>

## How HTTPS hosting works

When you run `host <port>`, the sandbox asks the Boat backend to create a stable
public route for that sandbox and port. The backend registers a subdomain using the
Boat's current machine address and the port you asked to expose.

The generated hostname is based on the sandbox subdomain plus the port:

```text theme={null}
https://<sandbox-subdomain>-<port>.on.boat.dev
```

Boat terminates TLS for `on.boat.dev`, then proxies each request to the target
sandbox over the exposed port. Your application still runs inside the sandbox; the public
HTTPS route sits in front of it. This is why your server must listen on `0.0.0.0`:
the route connects to the sandbox from outside the application process, not through
your app's local loopback interface.

<Note>
  Every hosted address is served under both `on.boat.dev` and `on.ascii.dev` (the name from before the rename) with the same label, so `SUBDOMAIN-3000.on.ascii.dev` and `SUBDOMAIN-3000.on.boat.dev` are the same route. Links you shared as `on.ascii.dev` keep working until that suffix is retired (see [Migrating from Box](/migrating-from-box)); switch what you publish to `on.boat.dev` whenever convenient.
</Note>

The sandbox authenticates to the Boat backend with its machine token, and the backend
performs the route registration. This keeps routing credentials out of the sandbox
environment while still letting the `host` CLI create, list, and remove routes.

## Host CLI reference

The `boat host` command runs from your local machine. The `host` CLI runs inside a sandbox. Both expose services from that sandbox on public HTTPS URLs.

<Warning>
  The service you expose must listen on `0.0.0.0`, not only on `localhost` or `127.0.0.1`.
</Warning>

### `boat host <sandbox-id> <port>`

Expose a running service without opening an interactive SSH session:

```bash theme={null}
boat host bx_f7k2q9hd 3000
boat host bx_f7k2q9hd 3000 --title "Login preview"
boat host bx_f7k2q9hd 3000 --private
boat host bx_f7k2q9hd 3000 --public
```

The command opens the firewall for that port, registers an HTTPS subdomain, and prints the URL. Calling it again for the same sandbox and port returns the same URL.

| Option | Description |
| - | - |
| `--title <title>` | Set the display title for the hosted port. |
| `--private` | Require the generated `_token` query parameter to access the URL. |
| `--public` | Clear any saved access token and return a URL that does not require `_token`. |

Use `--json` to print a machine-readable object with `sandboxId`, `port`, `url`, `access`, and `isProtected`.

It is a single API call, so it is also the fastest way to host a port from a script:

```bash theme={null}
curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/host" \
  -H "Authorization: Bearer $BOAT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"port":3000,"title":"Login preview"}'
```

Pass `{"public":true}` for an ungated URL.

### In-sandbox `host <port>`

Expose a running service on a stable HTTPS URL:

<CodeGroup>
  ```bash CLI theme={null}
  host 3000
  host 3000 --title "Login preview"
  host 3000 --private
  host 3000 --public
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/commands" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"host 3000 --title \"Login preview\""}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.command({ sandboxId: "bx_f7k2q9hd", command: "host 3000" });
  await sandbox.command({ sandboxId: "bx_f7k2q9hd", command: 'host 3000 --title "Login preview"' });
  await sandbox.command({ sandboxId: "bx_f7k2q9hd", command: "host 3000 --private" });
  await sandbox.command({ sandboxId: "bx_f7k2q9hd", command: "host 3000 --public" });
  ```

  ```python Python theme={null}
  sandbox.command("bx_f7k2q9hd", CommandRequest(command="host 3000"))
  sandbox.command("bx_f7k2q9hd", CommandRequest(command='host 3000 --title "Login preview"'))
  sandbox.command("bx_f7k2q9hd", CommandRequest(command="host 3000 --private"))
  sandbox.command("bx_f7k2q9hd", CommandRequest(command="host 3000 --public"))
  ```
</CodeGroup>

The command opens the firewall for that port, registers an HTTPS subdomain, and prints
the URL. Calling it again for the same port returns the same URL. A Sandbox can host up to
50 ports.

If you only need raw access without HTTPS or a subdomain, you can instead open the port
yourself with `ufw` and use `http://<sandbox-ip>:<port>` directly.

| Option | Description |
| - | - |
| `--title <title>` | Set the display title for the hosted port. |
| `--private` | Require the generated `_token` query parameter to access the URL. |
| `--public` | Clear any saved access token and return a URL that does not require `_token`. |

By default, `host <port>` creates a protected URL with an `_token` query parameter.
That protection is sticky: hosting the same port again returns a URL with the same
`_token` query parameter unless you pass `--public`.

### `host list`

Show hosted ports for the current sandbox:

<CodeGroup>
  ```bash CLI theme={null}
  host list
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/commands" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"host list"}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.command({ sandboxId: "bx_f7k2q9hd", command: "host list" });
  ```

  ```python Python theme={null}
  sandbox.command("bx_f7k2q9hd", CommandRequest(command="host list"))
  ```
</CodeGroup>

Protected ports are shown as `(gated)`. `host list` does not print the access token.
Use `host url <port>` to print the full URL with `_token=...`.

### `host url <port>`

Wait until the HTTPS URL is ready, then print it:

<CodeGroup>
  ```bash CLI theme={null}
  host url 3001
  host url 3001 --public
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/commands" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"host url 3001"}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.command({ sandboxId: "bx_f7k2q9hd", command: "host url 3001" });
  await sandbox.command({ sandboxId: "bx_f7k2q9hd", command: "host url 3001 --public" });
  ```

  ```python Python theme={null}
  sandbox.command("bx_f7k2q9hd", CommandRequest(command="host url 3001"))
  sandbox.command("bx_f7k2q9hd", CommandRequest(command="host url 3001 --public"))
  ```
</CodeGroup>

For a protected port, this prints the full token-gated URL:

```text theme={null}
https://<sandbox-subdomain>-3001.on.boat.dev?_token=<access-token>
```

This is useful when one service needs another service's public URL:

<CodeGroup>
  ```bash CLI theme={null}
  BACKEND_URL=$(host url 3001)
  ```

  ```bash curl theme={null}
  BACKEND_URL=$(curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/commands" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"host url 3001"}' | jq -r '.stdout')
  ```

  ```ts TypeScript theme={null}
  const result = await sandbox.command({ sandboxId: "bx_f7k2q9hd", command: "host url 3001" });
  const backendUrl = result.stdout.trim();
  ```

  ```python Python theme={null}
  result = sandbox.command("bx_f7k2q9hd", CommandRequest(command="host url 3001"))
  backend_url = result.stdout.strip()
  ```
</CodeGroup>

### `host hide <port>`

Take down the public URL:

<CodeGroup>
  ```bash CLI theme={null}
  host hide 3000
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/commands" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"host hide 3000"}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.command({ sandboxId: "bx_f7k2q9hd", command: "host hide 3000" });
  ```

  ```python Python theme={null}
  sandbox.command("bx_f7k2q9hd", CommandRequest(command="host hide 3000"))
  ```
</CodeGroup>

This closes public access and unregisters the HTTPS route. It does not stop the local
server process. Stop the server separately when you are done. Access tokens are preserved,
so hosting the same port again keeps existing protected links valid.

To get an ungated URL after a port has become protected, run `host <port> --public`.

## Firewall rules inside a sandbox

You have root, so you can add any firewall rule. Boat reaches the sandbox two ways, and a rule that blocks either one marks the sandbox degraded:

| What | Protocol | Runs as |
| - | - | - |
| Boat agent (commands, snapshots, health) | TCP port 8911 | `user` |
| Tunnel for HTTPS URLs and the agent | WireGuard, outgoing UDP to the address in `sudo wg show wg0 endpoints` | kernel, but carries packets from `user` processes |

Linux tags a tunnel packet with the process that sent the data inside it, so an owner rule on `user` also hits the tunnel. To block outgoing UDP for `user` without breaking Boat, allow the tunnel endpoint first:

```bash theme={null}
WG=$(sudo wg show wg0 endpoints | awk '{print $2}' | cut -d: -f1)
sudo iptables -I OUTPUT 1 -m owner --uid-owner user -p udp ! -d 127.0.0.0/8 -j REJECT
sudo iptables -I OUTPUT 1 -p udp -d "$WG" -j ACCEPT
```

A sandbox that answers on any route is never stopped or marked `error` for this. It shows `"health": "degraded"` with a reason, and sends [`sandbox.degraded`](/webhooks#events).

## Related

* [SSH Access](/ssh-access)
* [Environments](/environments)
* [Long-Running Tasks](/long-running-tasks)
* [Desktop Streaming](/desktop-streaming)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.