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 localopenboxbinary forca-keygen.
1. Create the database
In Neon, create a database and copy its connection string. It looks like:
postgres://user:[email protected]/openbox?sslmode=requireThe control plane creates its own tables on first boot — no manual migration.
2. Mint the CA key
openbox ca-keygen > openbox-ca.pem # keep this secret + backed upFor 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):
openbox ca-keygen | base64 | tr -d '\n' # paste this into .env.production3. 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:
npm --prefix cloudflare ci # installs the repo-local wranglercp .env.production.example .env.production # gitignored — never commit it$EDITOR .env.production # paste the Neon URL, the CA PEM, # and (optional) the mesh authkeyee verify # check required vars are presentmake push-secrets DRY=1 # previewmake push-secrets # wrangler secret put × 3OPENBOX_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
cd cloudflare && npm ciwrangler secret put DATABASE_URL # paste the Neon URLwrangler secret put OPENBOX_CA_KEY # paste the contents of openbox-ca.pemwrangler secret put OPENBOX_MESH_AUTHKEY # optional4. First deploy
wrangler deploywrangler 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.netto theopenbox-controlWorker. - Docs — create a Pages project named
openbox-docs(the docs workflow deploys to it) and binddocs.opbx.netto 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:
wrangler tail openbox-control --format pretty# look for: openbox login --server https://opbx.net --token obx_…Then, from any machine:
openbox login --server https://opbx.net --token obx_…openbox whoamiIf 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):
| Secret | Value |
|---|---|
CLOUDFLARE_API_TOKEN | a token with Workers Scripts, Containers, and Pages edit permissions |
CLOUDFLARE_ACCOUNT_ID | your Cloudflare account id |
Create the API token at Cloudflare → My Profile → API Tokens, then:
cp .env.ci.example .env.ci # gitignored$EDITOR .env.ci # paste the token + account idee 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, orcloudflare/triggersdeploy-control.yaml→ rebuilds + redeploys the container. - Pushing changes to
docs/orwebsite/triggersdeploy-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 controluses a local SQLite file by default. The DSN inOPENBOX_DB(or--db) selects SQLite (a path) vs Postgres (apostgres://URL).