Skip to content

Hosted deployment

This is how opbx.net runs — a hosted control plane you don’t have to keep a machine online for. The architecture:

opbx.net ─► Cloudflare Worker ─► Container (Durable Object)
│ runs `openbox control` (Go)
├─► Neon Postgres (registry/sessions/tokens)
└─► CA key (secret env var)
docs.opbx.net ─► Cloudflare Pages (this site)

The container is stateless: all durable state lives in Neon, and the SSH CA key is injected as a secret. That’s what lets it run on Cloudflare Containers, whose disk resets on every restart.

The CA key is the root of trust

Whoever holds the CA private key can mint user certs for every node. Store it only as a Cloudflare secret, and keep an encrypted backup somewhere safe. Rotating it invalidates all node host certs (nodes must re-enroll).

Prerequisites

  • A Cloudflare account (Workers Paid plan — Containers requires it).
  • A Neon project (free tier is fine).
  • wrangler (npm i -g wrangler) and a local openbox binary for ca-keygen.

1. Create the database

In Neon, create a database and copy its connection string. It looks like:

postgres://user:pass@ep-xxx.region.aws.neon.tech/openbox?sslmode=require

The control plane creates its own tables on first boot — no manual migration.

2. Mint the CA key

Terminal window
openbox ca-keygen > openbox-ca.pem # keep this secret + backed up

For the OPENBOX_CA_KEY secret, use the base64 form (a PEM is multi-line and .env files / secret stores can’t carry that; the control plane accepts either form):

Terminal window
openbox ca-keygen | base64 | tr -d '\n' # paste this into .env.production

3. Set the secrets

The three Worker secrets are managed with ee, whose schema lives in schema.yaml and whose Cloudflare push origin is configured in .ee. Copy the example, fill in real values, and push — one command replaces three wrangler secret put invocations:

Terminal window
npm --prefix cloudflare ci # installs the repo-local wrangler
cp .env.production.example .env.production # gitignored — never commit it
$EDITOR .env.production # paste the Neon URL, the CA PEM,
# and (optional) the mesh authkey
ee verify # check required vars are present
make push-secrets DRY=1 # preview
make push-secrets # wrangler secret put × 3

OPENBOX_CA_KEY is the multi-line PEM from step 2 (openbox ca-keygen), and OPENBOX_MESH_AUTHKEY is optional — leave it empty unless your nodes are on a Tailscale/Headscale mesh you want the web console to reach.

make push-secrets runs ee push cloudflare production with the repo-local wrangler (from cloudflare/node_modules) on PATH, so no global install is needed — just make sure it’s authenticated (ee auth wrangler, or npx --prefix cloudflare wrangler login).

Without ee — set the secrets manually
Terminal window
cd cloudflare && npm ci
wrangler secret put DATABASE_URL # paste the Neon URL
wrangler secret put OPENBOX_CA_KEY # paste the contents of openbox-ca.pem
wrangler secret put OPENBOX_MESH_AUTHKEY # optional

4. First deploy

Terminal window
wrangler deploy

wrangler builds the container image from the repo-root Dockerfile, pushes it to Cloudflare’s registry, and rolls out the Worker + container Durable Object. After this, deploys happen automatically from GitHub Actions (see step 7).

5. Bind the domains

  • App — in the Cloudflare dashboard, add a custom domain / route mapping opbx.net to the openbox-control Worker.
  • Docs — create a Pages project named openbox-docs (the docs workflow deploys to it) and bind docs.opbx.net to it.

6. Grab the bootstrap token

On the very first boot with an empty database, the control plane creates a user and prints a login command. Read it from the container logs:

Terminal window
wrangler tail openbox-control --format pretty
# look for: openbox login --server https://opbx.net --token obx_…

Then, from any machine:

Terminal window
openbox login --server https://opbx.net --token obx_…
openbox whoami

If you miss the token, delete the row in Neon (DELETE FROM users;) and redeploy to re-bootstrap.

7. Wire up CD

CD needs two GitHub Actions secrets, which ee also manages (a second env ci pushed to a github origin):

SecretValue
CLOUDFLARE_API_TOKENa token with Workers Scripts, Containers, and Pages edit permissions
CLOUDFLARE_ACCOUNT_IDyour Cloudflare account id

Create the API token at Cloudflare → My Profile → API Tokens, then:

Terminal window
cp .env.ci.example .env.ci # gitignored
$EDITOR .env.ci # paste the token + account id
ee push github ci # sets both as repo secrets (individual mode)

(Or add them manually under GitHub → Settings → Secrets → Actions.) From then on:

  • Pushing changes to the Go control plane, Dockerfile, or cloudflare/ triggers deploy-control.yaml → rebuilds + redeploys the container.
  • Pushing changes to docs/ or website/ triggers deploy-docs.yaml → redeploys this docs site to Cloudflare Pages.

Notes

  • Web console reachability. The browser console proxies exec through the control plane to a node. A Cloudflare-hosted control plane can only reach nodes that are publicly reachable or on a shared mesh — set OPENBOX_MESH_AUTHKEY (step 3) so the container joins your overlay. Plain CLI dispatch is unaffected: the CLI talks to nodes directly, the control plane only brokers identity.
  • Self-hosting instead. The same binary runs the control plane anywhere with a disk: openbox control uses a local SQLite file by default. The DSN in OPENBOX_DB (or --db) selects SQLite (a path) vs Postgres (a postgres:// URL).