Skip to content
sproutboat
Esc
navigateopen⌘Jpreview
On this page

Standalone binaries

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.

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

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

  • 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).

Was this page helpful?