---
title: Standalone binaries
description: "Run Sproutboat without a control plane: one executable, its own bindings, no broker required."
---

You can run Sproutboat without the control plane. `sproutboat build --standalone` emits **one executable** carrying its own bindings. Copy it to a Linux machine and run it — no broker or Bun required on the target.

<CodeGroup>

```sh Bun
bunx sproutboat build --standalone     # ~2 MB, SQLite and TLS compiled in
PORT=3000 ./dist/hello
```

```sh npm
npx sproutboat build --standalone      # ~2 MB, SQLite and TLS compiled in
PORT=3000 ./dist/hello
```

</CodeGroup>

This is a good option for a Pi, home server, single VM, or any deployment where you want to own the process directly. The same handler can move to a managed Sproutboat node later with `sproutboat deploy`, unchanged.

## Configuration

A standalone binary never sees `argv`, so there are no flags — everything arrives at runtime through the environment:

| Variable | Purpose |
| --- | --- |
| `PORT` | Port to listen on (default `8080`). |
| `SB_DATA_DIR` / `SPROUTBOAT_DATA` | Where state lives (default `./<name>.data`). |
| `SB_TRUSTED_PROXIES` | Reverse-proxy CIDRs / IPs. When the peer is one of them, `request.cf.clientIp` resolves from `X-Forwarded-For` (right-most untrusted entry); otherwise the header is ignored. |
| `SB_CA_BUNDLE` | Extra certificate authorities, on top of the compiled-in Mozilla roots. Adds trust; nothing disables verification. |
| *(secrets)* | Read by name from the environment, then `<data>/secrets.json`. A missing one does not stop the binary from starting; it throws the first time the handler reads it. |

## State on disk

```
<name>.data/
  store.sqlite          kv · r2 · mq · do_storage · do_alarm · ae
  d1/<binding>.sqlite   one file per D1 database
```

D1 is separate because it runs your SQL: a `CREATE TABLE kv (...)` in a handler would otherwise collide with the platform's own tables. This is the same layout `sproutboat dev` writes.

## What differs from a deployed sprout

:::warning
A standalone binary is not a drop-in replacement for a managed deployment. The differences below matter before you commit to one.
:::

- **Inbound TLS is not its job.** It serves plain HTTP on `$PORT`. Put Caddy, nginx, or a tunnel in front.
- **Outbound TLS is.** `fetch("https://…")` verifies against a compiled-in Mozilla root set.
- **Triggers stay internal.** Cron, queue batches, and alarms run on timers in the process, and a trigger arriving over the network is refused: with no broker, no token exists to check.
- **No service bindings.** They route through an edge, which one binary lacks.
- **Assets compile in**, capped at 8 MB. Past that, serve them from R2 or put a web server in front.
- **`request.cf.clientIp`** is the connection's remote address, resolved through `SB_TRUSTED_PROXIES` and `X-Forwarded-For` when set. `env.<D1>.backup()` writes its snapshot under `<data>/backups/`.
- **One thread.** The event loop overlaps connection I/O, but the handler and every embedded binding call run one at a time. Per core that is a throughput ceiling of about `1 / handler-time`; for more, run several copies on the same `$PORT` and `$SB_DATA_DIR` behind `SO_REUSEPORT` (Linux load-balances across them, SQLite WAL handles the shared files).
