BrowserBox Documentation

11 Run & deploy

Fleet: A Pool of Ephemeral Sessions (bbx fleet)

Customer guide · v19.3.1 · September 29, 2026

In this chapter

Fleet turns a sufficiently large Linux machine into a pool of ephemeral, clean-slate BrowserBox sessions. It is the machine-level deployment mode for applications that need to hand each authenticated end user a fresh, disposable browser session — without creating static BrowserBox tokens per user, and without your application ever managing Linux users, ports, nginx, or browser profiles.

Fleet is Linux only and requires a privileged operator (root, or a non-root account with passwordless sudo). Fleet seat users themselves never receive sudo.

11.1 The mental model #

Do not mint static BrowserBox tokens for every end user of your application. Instead, your application allocates a session at the moment it is needed:

application authentication succeeds
        |
bbx fleet acquire --json --json-schema 2
        |
redirect user to login_url
        |
session closes or expires
        |
bbx fleet release <allocation_id>

The login token inside login_url is a fresh, allocation-scoped connector capability — it is not your application’s user identity and is not consumed on first use. Your application remains responsible for its own authentication (Turnstile/CAPTCHA, SAML or OIDC, JWT validation, authorization), for its own session-timeout and disconnect detection, and for calling fleet release when a session ends. Fleet’s runtime monitor is a recovery path for BrowserBox processes that exit independently; it does not replace application-level release. Fleet handles everything below that line: Linux seat users, BrowserBox setup, allocation-scoped tokens, ports, nginx routing, startup, shutdown, clean-slate profile lifecycle, and local allocation locking.

11.2 The three layers #

  • Fleet deployment — machine-level configuration: the seat pool, port range, public hostname and routing topology, wildcard TLS, fleet-wide BrowserBox defaults, clean-slate policy.

  • Fleet seat — a reusable OS identity (e.g. bbx-seat-0000) that can run exactly one allocation at a time. Seats are an implementation detail; integrate against allocation IDs, not usernames.

  • Fleet allocation — one ephemeral session: a reserved seat, a reserved port set, a fresh login token, a clean browser profile, one public login URL, and one opaque allocation ID.

11.3 Connector and co-browsing semantics #

The Fleet allocation is the isolation boundary. Its login_url may be used repeatedly for reconnects while that allocation is alive. If more than one client receives the same URL, those clients join the same remote browser and see and control the same tabs, cookies, and page state, up to the configured BBX_MAX_CONNECTIONS cap. This is intentional co-browsing, not the creation of another isolated session and not the consumption of another Fleet seat.

Treat login_url as a bearer secret: possession is sufficient to join the allocation. Do not distribute one allocation’s URL to unrelated users. For one isolated member session per user, acquire once per member session and deliver that URL only to that authorized client. Release or automatic recovery stops the runtime and removes its published login capability; a later acquisition, even on the same reusable seat, receives a different random token.

11.4 Requirements #

  • Linux, with root or passwordless sudo for the operator.

  • An installed BrowserBox and a license with enough seats for your target concurrency, configured for the privileged operator that runs Fleet.

  • Machine sizing appropriate to concurrency (each live session runs a real Chrome).

  • For production subdomain routing: wildcard DNS (*.your-domain), a wildcard TLS certificate, nginx, and public port 443.

  • An internal port range for BrowserBox backends (default 7000–20000, loopback only in subdomain mode).

  • On PipeWire hosts, BrowserBox’s system pipewire-pulse drop-in. This is installed automatically by bbx update and full installation; fleet init and fleet doctor verify it before sessions are allocated.

11.5 Privileged operator and licence #

Fleet deployment state and licence configuration belong to the operating-system identity that runs bbx fleet. In the common sudo bbx fleet ... arrangement, that identity is root. A key previously certified only as the unprivileged browserbox user is intentionally not read from that user’s private configuration.

Configure the key once in the privileged operator context before the first acquisition, then confirm the doctor check passes:

sudo -H bbx certify
sudo bbx fleet doctor

If the privileged command must use a forward proxy, pass the standard proxy environment while certifying. The key is still entered at the interactive prompt and need not appear in shell history:

sudo -H env \
  http_proxy="$http_proxy" https_proxy="$https_proxy" \
  HTTP_PROXY="$HTTP_PROXY" HTTPS_PROXY="$HTTPS_PROXY" \
  no_proxy="$no_proxy" NO_PROXY="$NO_PROXY" \
  bbx certify

11.6 Initialization #

sudo bbx fleet init \
  --size 100 \
  --domain browser.example.com \
  --routing subdomain \
  --subdomain-mode random \
  --cert-file /etc/browserbox/fullchain.pem \
  --key-file /etc/browserbox/privkey.pem

Initialization is idempotent: re-running it creates only missing seat users, increasing --size adds seats, and decreasing --size never deletes users (excess seats simply become ineligible for new allocations). The nginx configuration is replaced atomically after certificate and DNS validation.

Fleet enables systemd lingering for every new or existing managed seat user so PipeWire or PulseAudio remains available on a headless host. On PipeWire systems, initialization also refuses to complete unless the BrowserBox system audio drop-in is installed. If this check fails after an upgrade, run browserbox --install (the same command-install step normally performed by bbx update), then repeat the original fleet init command. A full machine installation is not required.

A successful subdomain initialization installs and reloads the complete Fleet nginx configuration; a separate fleet routing apply is not required. For automation, add --json. Both success and failure then produce one structured result on standard output while operational detail remains on standard error.

If a first initialization stops after creating users but before routing is complete, the users and stable seat records are retained for the retry. This is safe preparatory state, not an allocatable Fleet: fleet doctor reports Fleet deployment ready for acquire as failed, and fleet acquire returns fleet_not_ready. Correct the reported prerequisite and re-run the complete fleet init command with the same routing and certificate options.

Each seat receives a stable main port and, in subdomain mode, stable public hostnames, so nginx is configured completely at init time — acquire and release never touch DNS, certificates, or nginx.

For conventional browsers, Fleet’s distinct service hostnames retain the normal iframe-authenticated WebSocket/WebAudio path; the service-name map carried by the login flow supplies the otherwise unrelated main and audio origins. Tor mode retains its direct WAV transport. Applications should therefore use the returned login_url unchanged rather than reconstructing service URLs.

11.7 DNS #

Subdomain routing requires a wildcard record pointing at the machine:

Type: A
Name: *.browser.example.com
Value: 203.0.113.10

Add a matching AAAA record if the machine serves IPv6. fleet init prints the exact records required, then validates two distinct probe subdomains before touching nginx. If your traffic deliberately arrives through a load balancer or proxy (so DNS does not resolve directly to this machine), pass --allow-proxied-domain; Fleet then distinguishes “DNS resolves successfully” from “DNS is confirmed to resolve directly to this machine” rather than silently waiving validation.

11.8 Certificates #

Subdomain routing needs a certificate covering *.your-domain (and ideally the base domain). Supply an existing wildcard certificate with --cert-file/--key-file; Fleet validates existence, key permissions, PEM type, cert/key match, expiry, and wildcard SAN coverage. Because wildcard issuance requires DNS validation, Fleet does not attempt automated wildcard issuance; issue the certificate through your DNS provider (for example certbot DNS-01) and hand the files to fleet init. Local/test domains (*.test, *.localhost, etc.) get locally generated certificates automatically.

The key must be the TLS private key belonging to the certificate, not an SSH key. Its first line is normally BEGIN PRIVATE KEY, BEGIN RSA PRIVATE KEY, or BEGIN EC PRIVATE KEY; BEGIN OPENSSH PRIVATE KEY is not accepted by nginx. Keep the key owner-only or group-read-only (normally mode 600 or 640), then verify the material before initialization:

sudo chmod 600 /etc/browserbox/privkey.pem
sudo openssl pkey -in /etc/browserbox/privkey.pem -noout -check

sudo openssl x509 -in /etc/browserbox/fullchain.pem \
  -pubkey -noout | openssl sha256
sudo openssl pkey -in /etc/browserbox/privkey.pem \
  -pubout | openssl sha256

The two SHA-256 values must match. Fleet reports distinct errors for an unreadable X.509 certificate, an OpenSSH or otherwise invalid TLS key, unsafe key permissions, a certificate/key mismatch, expiry, and missing wildcard SAN coverage.

The supplied paths remain the certificate source of record. Fleet installs a root-owned copy for nginx under its system configuration tree, with restrictive modes and the platform’s normal security context. This permits a source certificate to remain under an operator-controlled home or application directory on SELinux-enforcing RHEL-family systems. Fleet also applies the standard SELinux reverse-proxy allowance when required so nginx can reach the loopback BrowserBox backends.

After renewing or replacing the source files in place, refresh nginx atomically with:

sudo bbx fleet routing apply

11.9 Forward-proxy defaults for Fleet seats #

Fleet seats are separate operating-system users. Persist proxy variables through Fleet’s existing defaults surface so delegated setup, licence certification, BrowserBox services, Chrome, and restart guardians receive the same network policy on every acquisition:

sudo bbx fleet config set http_proxy  http://proxy.example.com:3128
sudo bbx fleet config set https_proxy http://proxy.example.com:3128
sudo bbx fleet config set HTTP_PROXY  http://proxy.example.com:3128
sudo bbx fleet config set HTTPS_PROXY http://proxy.example.com:3128

sudo bbx fleet config set no_proxy \
  localhost,127.0.0.1,::1,10.0.0.0/8,.internal.example.com
sudo bbx fleet config set NO_PROXY \
  localhost,127.0.0.1,::1,10.0.0.0/8,.internal.example.com

These values are stored in the operator-private Fleet state and are passed through the same delegated environment path as other Fleet-wide BrowserBox defaults. Use sudo bbx fleet config show to review them. This is distinct from the privileged operator environment used by fleet init, fleet doctor, and bbx certify; provide proxy variables to that operator process as shown above when it also requires proxy egress.

11.10 Routing modes #

  • subdomain (production default): every session is presented through port 443 under a per-seat hostname. Label styles: port (p7000.domain), seat (seat-0000.domain), or random (k7p4wm9f.domain — recommended: stable opaque hostnames, clean URLs). The hostname is stable per seat across allocations; the login token is fresh for every acquisition.

  • direct-port: the returned URL uses the machine hostname and the seat’s port directly (https://host:7000/login?token=...). Suitable for testing, internal deployments, or environments with another upstream routing layer. May require opening the port range in your firewall.

Routing is configured fleet-wide, not per acquisition: this keeps the session-start path fast (no DNS change, no certificate operation, no nginx reload) and keeps public URLs stable and auditable. Changing routing topology is a deployment-wide operation — bbx fleet routing apply refuses to run while allocations are active unless you explicitly pass --allow-active.

11.11 Acquire and release #

session_json="$(sudo -n bbx fleet acquire --json --json-schema 2 2>/dev/null)"

allocation_id="$(jq -r '.allocation.id' <<<"$session_json")"
login_url="$(jq -r '.allocation.login_url' <<<"$session_json")"

# Redirect the already-authenticated application user to $login_url.

sudo -n bbx fleet release "$allocation_id"

Schema 2 is the recommended integration contract. Acquire and single-allocation status return {ok, schemaVersion, allocation}, while list returns {ok, schemaVersion, allocations}. Every allocation uses the same fixed field set: id, state, health, seat, main_port, public_hostname, login_url, created_at, updated_at, and timeout_seconds; unavailable values are JSON null. Status-only probes live under allocation.diagnostics. Schema 1 remains the default during the compatibility window and can be selected explicitly with --json-schema 1.

In --json mode, stdout carries a single JSON document while diagnostics go to stderr. A terminal displays both streams together, which can make [fleet] progress lines appear to precede the JSON even though stdout remains valid. Capture stderr separately in production when its diagnostics matter; redirecting it as above is appropriate when only the result object is required. Errors carry stable codes such as fleet_not_ready, fleet_exhausted, fleet_license_unavailable, or fleet_public_route_failed. Acquisition succeeds only after the public login URL is verified live; any failure (license, setup, startup, routing) rolls the local reservation back.

The privileged operator licence is checked before a seat is reserved. If delegated BrowserBox setup itself fails, Fleet writes a bounded, secret-redacted diagnostic to fleet/diagnostics/<seat>-setup.log and names that path in the error response.

11.12 Keeping a small warm pool #

Fleet does not add a second “allocated but unassigned” state. A customer application can provide sub-second handout by acquiring a small number of normal allocations ahead of demand and recording them as warm in its own database. Size the deployment so that Fleet seats cover peak assigned sessions, warm stock, and operational headroom:

fleet size >= peak assigned + warm target + headroom

Each warm allocation consumes a Fleet seat and a licence seat. Its login_url is also a live bearer capability, so store it with the same care as any other session credential and never write it to application logs.

The idle timer makes warm stock bounded. BBX_SHUTDOWN_IDLE_MS starts when the BrowserBox runtime starts, before any client has connected. The first client connection cancels that timer; the timer starts again after the last client disconnects. A user who receives a healthy warm allocation therefore gets a fresh idle budget rather than the remainder of its pool dwell time.

Configure a stock lifetime comfortably longer than the expected dwell time and keep the existing Fleet monitor running as a recovery path:

# Example: allow warm stock to dwell for up to two hours.
sudo bbx fleet config set BBX_SHUTDOWN_IDLE_MS 7200000

# Run under systemd or the deployment's normal supervisor.
sudo bbx fleet monitor --interval 5 --grace 15

The application owns four bounded loops:

  1. Top up. Every few seconds, and immediately after a handout, acquire only the number needed to restore the warm target. Keep acquire concurrency small because acquisitions briefly share Fleet’s local lock.

  2. Hand out. Pop the oldest warm row, mark it assigned, and return its login_url. Before returning it, call bbx fleet status <allocation-id> --json --json-schema 2 and require allocation.health=healthy, allocation.diagnostics.login_link_present=true, and allocation.diagnostics.public_route_ok=true. Bound retries and fall back to a cold acquire if no healthy warm row remains.

  3. Rotate. Release and replace warm rows after roughly 75 percent of the configured idle budget so a user is not handed a session near shutdown.

  4. Return. On explicit logout, application timeout, or another terminal session event, call fleet release, remove the row, and trigger top-up.

Fleet health values are healthy, partial, down, and unknown; there is no up value. Reconcile the application’s rows against bbx fleet list --json --json-schema 2 before top-up in both directions: drop rows Fleet no longer has, and adopt or release live allocations left behind by an interrupted application write. This prevents dead handouts, under-provisioning against phantom rows, and stranded paid seats.

In a measured Rocky Linux 9 proof with SELinux enforcing, three cold acquires averaged 4.260 seconds while three warm handouts, including the health pre-flight, averaged 0.699 seconds. These values demonstrate the regimen rather than promise a universal latency; host CPU, storage, Chrome profile creation, and Fleet size affect cold acquisition time.

11.13 Calling Fleet from a web application #

No additional BrowserBox HTTP provisioning API is required. A web application on the Fleet host may execute the fixed argument vectors bbx fleet acquire --json --json-schema 2 and bbx fleet release <allocation-id> --json --json-schema 2 directly through a small privileged adapter. A remote application may call the same adapter over a local authenticated service boundary. Keep the adapter narrow:

  • expose only acquire, release, and optional status operations;

  • invoke bbx with an argument array, never a shell command assembled from request text;

  • authenticate and authorize in the application before acquisition;

  • validate allocation IDs before release and never accept a seat username, port, command, or file path from the request;

  • return login_url to the authorized caller without logging it; outside a warm-pool record, retain only allocation_id;

  • release on explicit logout and application timeout, with Fleet monitor/reap retained as recovery rather than the normal application lifecycle.

Generic fixed-hook runners such as adnanh/webhook can provide this transport when they are already part of the deployment. Bind the runner to loopback or a protected internal interface, require application authentication or mTLS at the edge, configure fixed hook commands with no request-derived shell arguments, and give it only the privilege needed for the exact Fleet commands. Do not expose a general command runner or the raw privileged bbx CLI to the network.

11.14 Clean-slate security #

Every allocation starts from a fresh browser profile by default (FLEET_CLEAN_SLATE=true), using BrowserBox’s canonical BBX_CLEAN_SLATE mechanism. Cookies, local storage, history, cache, saved authentication, IndexedDB, and service-worker state from a prior user of the same seat are not intended to survive seat reuse: release stops the runtime, removes the login capability, and wipes the browser profile; acquisition additionally starts with clean-slate enforced. Because different external users receive the same reusable seats over time, do not disable clean slate in multi-user deployments. Persistent per-user profiles are deliberately not part of this design.

11.15 Operational commands #

sudo bbx fleet list             # active allocations (no secrets)
sudo bbx fleet status [<id>]    # fleet summary or one allocation
sudo bbx fleet reap             # one dead-runtime recovery pass
sudo bbx fleet monitor          # continuous foreground recovery
sudo bbx fleet doctor           # environment / DNS / cert health
sudo bbx fleet reconcile --fix  # repair state drift safely
sudo bbx fleet routing show     # seat-to-hostname-to-port map
sudo bbx fleet routing apply    # atomic whole-of-fleet re-apply
sudo bbx fleet config set K V   # fleet-wide BrowserBox defaults

fleet status reports local capacity, fully down and partially healthy running allocations, and an advisory license-vacancy snapshot; local free seats do not guarantee globally free license seats — license certification during acquire remains authoritative.

11.16 Automatic dead-runtime recovery #

BrowserBox can shut itself down when a runtime has never received a client connection or after its last client disconnects; the main timeout is controlled by BBX_SHUTDOWN_IDLE_MS. The timer starts at runtime startup, is cancelled by the first client connection, and starts again after the last client disconnects. “Idle” does not mean lack of keyboard or mouse activity while a client stays connected, so a connected but untouched tab does not start this timer. A customer application that needs an input-inactivity or absolute-session limit must enforce that policy and call fleet release. Because idle shutdown happens inside the seat runtime, Fleet confirms the runtime is fully down and then returns the allocation through the same stop, login-capability removal, and clean-slate profile-wipe path used by an explicit fleet release.

Run the foreground monitor under your normal service supervisor:

# Persist the BrowserBox idle timeout for every Fleet seat (15 minutes).
sudo bbx fleet config set BBX_SHUTDOWN_IDLE_MS 900000

# Run this foreground process under systemd or your supervisor.
sudo bbx fleet monitor
# Optional:
sudo bbx fleet monitor --interval 5 --grace 15

A running allocation is eligible for automatic release only when its main port is no longer listening and the seat has no live BrowserBox or Chrome process. Zombie process-table entries do not count as a live runtime. A partial result (only the port or only a live process remains), or an unknown result caused by a failed process-table probe, is reported by fleet list and fleet status, but is not automatically released. The monitor observes process and socket health outside the short Fleet ownership lock, waits through the grace window, and then revalidates the allocation identity, state, runtime marker, and health before cleanup. A recovered or replaced runtime is therefore left alone, and a slow health probe does not block a simultaneous acquire. Stopping the monitor during that window cancels the in-progress pass rather than shortening the grace.

fleet reap performs one such pass. On its normal path, fleet acquire makes one locked pass over the seat and allocation records, reserves one seat atomically, and performs startup outside the lock; it does not scan the process table or wait for monitor work. If the records say the pool is exhausted, acquire performs one bounded recovery pass using the shorter acquire grace, retries reservation once, and then reports exhaustion. This just-in-time fallback recovers capacity if the monitor is temporarily unavailable while keeping the ordinary acquire path lightweight. Two simultaneous acquires cannot reserve the same seat or overlapping port set, and simultaneous releases are idempotent.

The monitor is quiet when no recovery candidates exist. A pass with candidates emits one summary rather than one success line per allocation. Failure detail is capped (10 allocations per pass by default), additional detail is summarized as suppressed, and repeated failed passes use exponential backoff capped at 300 seconds. Stale reserved, starting, or releasing records are owned by monitor/reap recovery after their transition TTL (900 seconds by default); acquire does not perform heavy stale-state repair on its normal path.

11.17 Security guidance #

  • login_url is a secret capability. Do not log it. Deliver it only to the authenticated user and then forget it; a warm-pool row is the exception and must protect the URL as a live credential until handout or release. Retain allocation_id for lifecycle operations.

  • The login URL is reusable for the life of its allocation and may admit co-browsers. Never describe it as a one-time-use credential or share it across users who require isolation.

  • Call fleet release on session timeout and explicit logout.

  • Fleet seat users must never have sudo.

  • Fleet state (~/.config/dosaygo/bbpro/fleet/) is operator-private (0700/0600).

  • Human-readable fleet list output never shows tokens; --json output (which does include login_url) is for the privileged operator only.

11.18 Reference architecture #

A typical hardened deployment in front of Fleet:

Cloudflare Tunnel / reverse proxy
        |
Turnstile
        |
SAML identity provider
        |
application authorization and JWT
        |
bbx fleet acquire          (this machine)
        |
BrowserBox public Fleet hostname (port 443)
        |
target pension/member portal

The SAML/JWT system and Fleet are separate layers: your application proves who the user is; Fleet hands that user one disposable browser session and takes it back afterwards.

BrowserBox · Published by DOSAYGOHappy browsing.