Operators

Self-hosting the coordinator

T73 (DAN-279) · opens M8 (enterprise). This guide shows how to run the NeuronPool coordinator on your own infrastructure — via Docker, Kubernetes (Helm), or your own Cloudflare account — including a fully air-gapped mode with no outbound calls to external SaaS.

The coordinator is a Cloudflare Worker (Hono) backed by D1 (SQLite), KV, and Durable Objects. Off Cloudflare it runs on the same runtime via Miniflare/workerd (wrangler dev --local), with D1/KV/DO state persisted to a local volume. No source changes are required to self-host — the packaging in deploy/ points the same workers/src entrypoint at a self-host Wrangler config.

Contents

Modes at a glance

DeploymentRuntimeStateBest for
Docker Composewrangler dev --local (workerd)SQLite/KV/DO on a Docker volumesingle-node, on-prem, air-gapped
Kubernetes (Helm)wrangler dev --local (workerd)SQLite/KV/DO on a PVCclustered on-prem, air-gapped
Cloudflare (self-account)Cloudflare WorkersD1 / KV / Durable Objectsyour own CF tenant

All three share one flag — AIR_GAPPED — that short-circuits outbound calls to external SaaS (Stripe, OpenRouter, Rekor/Sigstore, Hugging Face). It is additive and defaults off on the hosted control plane; the self-host packaging defaults it on.

Quick start: Docker Compose

Prerequisites: Docker with Compose v2. Run from the repository root:

docker compose -f deploy/docker/docker-compose.yml up --build

This builds deploy/docker/Dockerfile.selfhost, applies the D1 migrations from migrations/ to a local SQLite store, and starts the coordinator on port 8787 with state persisted to the neuronpool-data volume. Verify:

curl http://localhost:8787/health
# {"ok":true,"service":"neuronpool","name":"NeuronPool (self-hosted)"}

curl http://localhost:8787/.well-known/neuronpool.json

The container defaults to air-gapped (AIR_GAPPED=1). To run a connected self-host that keeps billing/distribution online, set AIR_GAPPED=0 and provide the relevant secrets (see Configuration reference) via an env file or your orchestrator's secret store — never commit real values.

How it runs

The image installs the pinned wrangler plus the platform workerd binary at build time, so nothing is downloaded at runtime. On start, deploy/docker/entrypoint.sh:

  1. wrangler d1 migrations apply neuronpool-db --local --persist-to /data/.wrangler-state
  2. wrangler dev --local --ip 0.0.0.0 --port 8787 --persist-to /data/.wrangler-state

using deploy/wrangler/wrangler.selfhost.jsonc.

Kubernetes (Helm)

The chart lives at deploy/helm/neuronpool/. It deploys a single-replica coordinator with a ConfigMap (non-secret env), an optional Secret (connected mode), a PersistentVolumeClaim for D1/KV/DO state, a Service, and an optional Ingress.

Build and push the image first, then install:

docker build -f deploy/docker/Dockerfile.selfhost -t <registry>/neuronpool-coordinator:selfhost .
docker push <registry>/neuronpool-coordinator:selfhost

helm upgrade --install neuronpool deploy/helm/neuronpool \
  --namespace neuronpool --create-namespace \
  --set image.repository=<registry>/neuronpool-coordinator \
  --set image.tag=selfhost \
  --set config.appUrl=https://neuronpool.internal.example.com

Render and inspect before applying:

helm lint deploy/helm/neuronpool
helm template neuronpool deploy/helm/neuronpool

Expose it with an Ingress:

helm upgrade --install neuronpool deploy/helm/neuronpool \
  --set ingress.enabled=true \
  --set ingress.className=nginx \
  --set ingress.hosts[0].host=neuronpool.internal.example.com \
  --set ingress.hosts[0].paths[0].path=/ \
  --set ingress.hosts[0].paths[0].pathType=Prefix

Connected mode (billing/distribution online) — provide secrets inline or via a pre-created Secret:

# Inline (rendered into a chart-managed Secret):
helm upgrade --install neuronpool deploy/helm/neuronpool \
  --set airGapped=false \
  --set secrets.STRIPE_SECRET_KEY=rk_test_... \
  --set secrets.STRIPE_WEBHOOK_SECRET=whsec_...

# Or reference an existing Secret with the same keys:
kubectl -n neuronpool create secret generic neuronpool-extra \
  --from-literal=STRIPE_SECRET_KEY=rk_test_...
helm upgrade --install neuronpool deploy/helm/neuronpool \
  --set airGapped=false --set existingSecret=neuronpool-extra

The coordinator keeps local D1/KV/DO state on a single ReadWriteOnce volume, so replicaCount is fixed at 1 and the Deployment uses the Recreate strategy. Do not scale it horizontally — see Limitations.

Your own Cloudflare account

To self-host on your own Cloudflare tenant (managed D1/KV/DO instead of local SQLite), use the same template deploy/wrangler/wrangler.selfhost.jsonc and fill in your account and resource ids:

# 1. Create resources on your account
wrangler d1 create neuronpool-db
wrangler kv namespace create STATS_KV
wrangler kv namespace create CANARY_KV

# 2. Paste the returned ids + your account_id into
#    deploy/wrangler/wrangler.selfhost.jsonc (uncomment account_id).

# 3. Apply migrations remotely and deploy
wrangler d1 migrations apply neuronpool-db --remote -c deploy/wrangler/wrangler.selfhost.jsonc
wrangler deploy -c deploy/wrangler/wrangler.selfhost.jsonc

# 4. Set secrets (connected mode only)
wrangler secret put STRIPE_SECRET_KEY -c deploy/wrangler/wrangler.selfhost.jsonc

Leave AIR_GAPPED=1 in the template for a locked-down deployment, or set 0 for a connected one.

This is not the hosted NeuronPool control plane. Do not run ./deploy.sh — that targets the primary hosted Worker. Never use --remote against resources you do not own.

Air-gapped mode

Air-gapped mode is for deployments with no route to the public internet. It is controlled by the additive AIR_GAPPED variable (workers/src/types.tsBindings.AIR_GAPPED).

  • Accepted "on" values (case/space-insensitive): 1, true, yes, on.
  • Anything else (including unset) is off — this preserves the hosted control plane's existing behaviour exactly.

When on, the guard in workers/src/env.ts reports the following external SaaS hosts as blocked so callers short-circuit them:

ServiceHostsWhat it gates
stripeapi.stripe.com, files.stripe.comCheckout, Connect payouts, Invoicing
openrouteropenrouter.ai, api.openrouter.aiOpenRouter distribution surface
rekorrekor.sigstore.dev, search.sigstore.devBlessed-build transparency-log re-verification
huggingfacehuggingface.co, api-inference.huggingface.coHugging Face model/provider fetches

Internal Durable Object fetches (https://job-bridge/…, https://account-lock/…) and the coordinator's own origin are always allowed — only the external providers above are short-circuited.

The guard exposes three helpers callers use before an outbound request:

import { isAirGapped, isEgressAllowed, assertEgressAllowed } from "./env";

if (isAirGapped(env)) { /* skip the optional external feature */ }

if (isEgressAllowed(env, url)) { await fetch(url); }      // branch
assertEgressAllowed(env, url);                            // or fail closed

Core inference — host register/heartbeat/lease, job routing, metering against local credits, pools, the OpenAI-compatible gateway, and the discovery document — has no external dependency and works unchanged in air-gapped mode.

Air-gapped operational notes

  • Billing: with AIR_GAPPED=1, do not configure Stripe. Credits are managed locally (seed/administer via the account/ledger paths). Keep PLATFORM_FEE_BPS=0.
  • Blessed builds: import release manifests from a file rather than fetching Rekor. The signature still verifies against the pinned RELEASE_PUBKEY; the Rekor transparency-log re-check is what's skipped offline.
  • Model artifacts: stage GGUF/MLX weights on the hosts out-of-band; the coordinator only brokers jobs and never downloads model files itself.
  • Egress policy: for defense in depth, also enforce the block at the network layer (Kubernetes NetworkPolicy / egress firewall) so the guard is not the only line of defense.
  • Host telemetry: POST /v1/telemetry is accepted but not stored when AIR_GAPPED=1. Hosts with NEURONPOOL_AIR_GAPPED=1 never send. See docs/provider/telemetry.md.

Configuration reference

Non-secret vars (Docker env / Helm config.* / Wrangler vars):

VarDefault (self-host)Purpose
AIR_GAPPED1Air-gapped mode flag (see above).
APP_NAMENeuronPool (self-hosted)Display name in /health and discovery.
APP_URLhttp://localhost:8787Public URL for discovery + redirects.
STRIPE_MODEtestStripe mode when connected.
PLATFORM_FEE_BPS0Platform fee in basis points.
PERSIST_TO/data/.wrangler-stateLocal D1/KV/DO state dir (Docker/K8s).

Secrets (connected mode only; set via wrangler secret put, a K8s Secret, or a Docker env file — never commit):

SecretPurpose
STRIPE_SECRET_KEYStripe Checkout + Connect. Prefer a restricted rk_… key.
STRIPE_WEBHOOK_SECRETVerifies Stripe webhooks.
RELEASE_PUBKEYEd25519 pubkey pinning blessed-build ingestion.
OPS_KEYBearer secret gating the ops channel report.
POE_ACCESS_KEYPoe server-bot access (distribution).

Migrations & upgrades

Migrations in migrations/ are additive and forward-only.

  • Docker/K8s (local store): the entrypoint runs wrangler d1 migrations apply neuronpool-db --local --persist-to <PERSIST_TO> on every start, so a newer image applies any new migrations against the same persisted volume automatically. Upgrade = pull the new image and restart (docker compose pull && up -d, or helm upgrade with the new image.tag).
  • Cloudflare self-host: run wrangler d1 migrations apply neuronpool-db --remote -c deploy/wrangler/wrangler.selfhost.jsonc before wrangler deploy.

Roll back by redeploying the previous image tag. Because migrations are additive, an older coordinator tolerates newer columns; still, snapshot the state volume (below) before a major upgrade.

Backup & restore

State lives entirely under PERSIST_TO (/data/.wrangler-state): the D1 SQLite database, KV, and Durable Object storage.

# Docker: snapshot the named volume
docker run --rm -v neuronpool-data:/data -v "$PWD:/backup" busybox \
  tar czf /backup/neuronpool-state-$(date +%F).tgz -C /data .

# Restore into a fresh volume
docker run --rm -v neuronpool-data:/data -v "$PWD:/backup" busybox \
  sh -c "cd /data && tar xzf /backup/neuronpool-state-YYYY-MM-DD.tgz"

On Kubernetes, snapshot the PVC (VolumeSnapshot if your CSI driver supports it, or tar from a debug pod mounting the claim). For a Cloudflare self-host, use D1 export (wrangler d1 export) and KV listing/backup on your account.

Limitations

  • Single node. Local D1/KV/DO state is a single SQLite/workerd store on one RWO volume, so the Docker/Helm coordinator runs one replica. For HA/scale, self-host on your own Cloudflare account (managed D1/KV/DO) instead.
  • wrangler dev runtime. The off-Cloudflare packaging uses wrangler dev --local (Miniflare/workerd) as the documented self-host runtime. A dedicated Node/Bun build with SQLite/Postgres store adapters is tracked separately (T73.1 store adapters) and is out of scope for this change.
  • Native rate-limiting bindings run under Miniflare in local mode; behaviour can differ slightly from Cloudflare's production limiter.
  • Air-gapped disables external features (Stripe billing, OpenRouter/HF distribution, Rekor re-verification). Core inference is unaffected.
  • Scheduled (cron) triggers are not fired automatically by wrangler dev; invoke the maintenance tick manually if needed (curl http://localhost:8787/cdn-cgi/local/scheduled) or run an external scheduler in air-gapped deployments.

Licensing

The coordinator (workers/) is source-available under BSL 1.1, converting to Apache-2.0 after four years (see workers/LICENSE and the README licensing section). Production self-hosting is subject to those BSL terms. The host agent, shared library, protocol, and SDKs are Apache-2.0.