Run your own hub
The hub is one container. It keeps its data in Postgres (or, for a small single-box install, in an embedded database on one volume). It needs a hostname with TLS in front, a reverse proxy that passes WebSockets, and outbound HTTPS to the AI provider.
Start it
docker run -d --name be-hub --restart unless-stopped \
-p 8001:8001 -v be-hub-data:/data \
-e PUBLIC_URL=https://hub.[yourdomain] \
-e ADMIN_PASSWORD='…' -e AGENT_TOKEN='…' -e ANTHROPIC_API_KEY='…' \
be-hub:latest # ask us for the image and registry, or build it from the repositoryPut TLS in front with Caddy, Traefik or nginx, and make sure the proxy passes WebSocket upgrades on /agent. Machines only need outbound 443 to that hostname.
Database
Set DATABASE_URL to a Postgres connection string and the hub keeps everything there: companies, users, machines, history, alerts, patches, scripts. Postgres 14 or newer; one database, one user that owns it; the hub creates and upgrades its own schema at start. Without the variable the hub uses an embedded database file on the data volume, which is fine for a single box you back up yourself.
Moving an existing hub to Postgres needs one change: add DATABASE_URL and restart. On the first start against an empty database the hub copies every table from the embedded file, advances the sequences, and renames the file .imported so it is never read again. Companies, enrollment tokens, users, machines and history all survive; agents reconnect as if nothing happened. The log says what was copied. To go back, remove the variable and rename the file.
Keep the database close to the hub (same host or same private network): the hub runs each query synchronously, so it is latency that matters, not throughput. Back up Postgres the way you back up any Postgres.
Environment
| variable | purpose | default |
|---|---|---|
| DATABASE_URL | a Postgres connection string (postgres://user:password@host:5432/db). Set it and the hub uses Postgres; leave it unset and the hub keeps an embedded database on the volume | embedded database |
| PUBLIC_URL | the address baked into install pages, scripts and agent configuration | required |
| ADMIN_PASSWORD | the admin user's password, applied on every start | generated once and printed if unset |
| ADMIN_API_KEY | the admin user's API key, applied on every start | generated once into the volume if unset |
| AGENT_TOKEN | the enrollment token of the default company on first boot | required on first boot |
| ANTHROPIC_API_KEY | the AI provider key BE AI uses | BE AI off if unset |
| CLAUDE_MODEL, CLAUDE_AUTORUN, CLAUDE_DAILY_RUNS, CLAUDE_DEDUPE_MIN | model, alert-driven runs on or off, cap per company per day, dedupe minutes | sonnet, on, 50, 30 |
| CLAUDE_SCHEDULE_TIME, CLAUDE_SCHEDULE_TZ | default nightly pass time and time zone for companies that set none | 02:00, Australia/Melbourne |
| CLAUDE_PRICE_IN_PER_M, CLAUDE_PRICE_OUT_PER_M, CLAUDE_PRICE_CURRENCY | cost estimates in the console | 3, 15, USD |
| OFFLINE_GRACE_MIN, ALERT_NOTIFY_DELAY_SEC | downtime grace period; delay before the first notification | 5, 60 |
| EMAIL_API_KEY, EMAIL_FROM, EMAIL_API_URL | email alerts | off |
| TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM | SMS and phone-call alerts | off |
| SAMPLE_RETENTION_DAYS, EVENT_RETENTION_DAYS, HOURLY_RETENTION_DAYS | retention | 14, 365, 730 |
| PORT | listening port inside the container | 8001 |
What is on the volume
hub.db (the database, WAL mode), audit.jsonl (append-only audit trail), releases/ (signed agent releases and notes). Back up the volume; a nightly copy of those files is enough. Restore is a copy back.
Upgrades
Pull the new image and restart. Schema changes apply themselves at start. Agents reconnect within two minutes; runs in flight at the moment of restart are closed out and marked, and conversations resume. Pick a quiet time.
Moving from hosted to self-hosted
Ask for a copy of your volume. Start your hub with it, set PUBLIC_URL to the new hostname, and run each company's install line with the apply option on its machines so they point at the new address. Identities and history carry over.
Sizing
A small VM with 2 vCPU and 2 GB of memory serves hundreds of machines. Heartbeats are under 1.5 KB every five minutes; history is rolled up hourly.
Moving the hub to a new address
Every agent holds the hub's address in its own configuration, so a new hostname is a fleet change, not a DNS change. The order matters:
- Make the new name serve the hub alongside the old one: add the domain to the hub's reverse proxy so it gets a certificate, and set
PUBLIC_URLto the new address so install links, install scripts and updates use it from then on. Keep the old name resolving. - Roll an agent that has
set_serverthrough the old name from Updates; agents older than that cannot be re-pointed remotely and would need the install line with apply on each machine. - Press re-point on the hub address panel under Updates. Each online machine is told the new address, writes its own configuration, answers, and reconnects through the new name a few seconds later; the panel groups machines by the name they connect through, so you can see who has moved. Machines offline at the time are re-pointed when you press it again while they are on.
- Retire the old name only when the panel shows nobody connecting through it.
An agent accepts only a wss://host/agent address; it refuses to move to plain ws:// or to any other path, so a mistake in the address is rejected rather than stranding the machine. Every re-point is in the audit trail with the person who did it.
Try it on one machine first.
The install line takes a minute. Uninstall the agent and you are out.