Self-hosted Durable Objects, operated by Kubernetes.
celld-operator runs Cloudflare Workers and Durable Objects on your own cluster with celld, and supplies everything celld deliberately leaves out: fleet provisioning, ingress, gated rollouts, autoscaling, and tenancy. One custom resource, one complete platform.
$ helm install celld-operator \
oci://ghcr.io/ezgamehost/charts/celld-operator \
--namespace celld-operator-system --create-namespace
Stateless self-hosted Workers already exist. This is the stateful part.
workerd gives you self-hosted stateless Workers. What nobody else offers is distributed, durable, self-hosted Durable Objects: your agents, rooms, documents, and tenants as named stateful cells on infrastructure you control, with RPO=0 durability against an object-storage bucket you own.
Every object is its own database
Each Durable Object ("cell") is a private SQLite database, replicated to your bucket before any write is acknowledged. Node loss cannot lose an acknowledged write.
One CRD, an entire fleet
A single WorkerApp resource reconciles the StatefulSet, Services, ingress, NetworkPolicies, PodDisruptionBudget, mesh policy, metrics, and autoscaler for one application.
Operational rules, encoded
celld's drain semantics, rollout gating, and memory bounds are enforced by the reconciler. There are no templates to misconfigure. The controller cannot be talked into unsafe states.
Nodes coordinate through a bucket. The operator does the rest.
celld nodes share one object-storage bucket (deployments, per-cell SQLite state, and ownership leases) with no consensus service. Object-storage compare-and-swap guarantees exactly one node owns a cell at a time.
The operator turns one WorkerApp into one fleet of celld pods sharing a
bucket prefix: routed through your Gateway, guarded by network policy, scaled by
KEDA on cell-utilization metrics, and rolled out with celld's documented
fleet-wide restoring=0 gate.
Each application is its own fleet with its own bucket prefix and credentials, so the blast radius of even a full runtime compromise is that one app. Read the architecture →
One resource. A whole platform.
Declare hostnames, the app version, the bucket, sizing, and scaling targets.
The operator reconciles the workload, networking, security policy, ingress,
rollout gating, metrics, and autoscaling, then reports fleet health back
in status.
$ kubectl get workerapps -n tenant-acme
NAME PHASE APP READY RESTORING
chat Ready sha-abc123 3 0
apiVersion: celld-operator.io/v1alpha1
kind: WorkerApp
metadata:
name: chat
namespace: tenant-acme
spec:
hostnames: ["chat.acme.example.com"]
appVersion: sha-abc123 # or "auto": follow the bucket
celld:
image: ghcr.io/denoland/celld:v0.2.0
replicas: 3
bucket:
name: s3://platform-cells/apps/chat
credentialsFrom:
iamRole: arn:aws:iam::123456789012:role/celld-chat
resources:
memoryGi: 8 # ~1,000 resident cells
maxResidentCells: 1000
websockets: true
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targets:
residentCellUtilization: 70
telemetry:
enabled: true
retention: 30d
The control plane celld deliberately ships without
celld supplies the runtime and the data plane; the operator supplies the platform. Upstream stays unpatched: the operator's celld-facing surface is env vars, signals, and two HTTP endpoints.
Gated rollouts
Not a vanilla rolling update: the operator releases one pod at a time and steps only when the whole fleet reports restoring=0. The restore load lands on peers, which a stock rollout cannot see. Progress lives in status.rollout.waitingOn.
Custom-metrics autoscaling
CPU is the wrong signal for celld. The operator exports resident-cell utilization and pressure-shedding metrics from each pod's /state, and drives a KEDA ScaledObject with celld-aware policy: scale up early, scale down slowly, never during a rollout.
Ingress your way
Gateway API HTTPRoutes on a shared Gateway, classic Istio VirtualServices, or plain networking.k8s.io/v1 Ingress with cert-manager TLS. Drain 503s are retried at the edge so rollouts are invisible to clients.
Security by default
celld's unauthenticated internal listener is fenced by NetworkPolicy and Istio AuthorizationPolicy, never routed. One IAM role per fleet, scoped to that fleet's bucket prefix. Tenancy is enforced at the Kubernetes layer.
GitOps-native deploys
Pin appVersion in git and let your GitOps tool roll the fleet, or set appVersion: auto and a plain celld deploy rolls it within one poll interval. Version drift always surfaces as a status condition.
Built-in telemetry
One field enables celld's tracing: Parquet traces and logs in your own bucket (DuckDB-queryable, no services to run) or OTLP to your collector. W3C traceparent joins edge spans to Durable Object spans.
Rules you cannot get wrong
celld has precise operational requirements. Most platforms put them in a runbook; this operator puts them in the reconciler. There are no templates to drift.
- Liveness is TCP only. celld's health path answers 503 during a graceful drain; an HTTP liveness probe there would kill nodes mid-handoff.
- Termination grace exceeds the drain bound, so the kubelet never SIGKILLs a draining node.
CELLD_MAX_RSS_MBis set explicitly to ~80% of the container memory limit, because the upstream default is not cgroup-aware.- The internal listener stays internal. Port 8081 is reachable only from fleet pods and the operator, and is never routed.
- Peers can reach draining pods so cell handoff works while a node reports unready.
- Breaking celld upgrades are refused unless the resource explicitly opts into a Recreate, so a GitOps diff alone can never create a mixed fleet.
The runtime underneath
celld is Deno Land's open-source
(Apache-2.0) daemon that embeds V8 and executes Wrangler-built Workers. Your
existing build pipeline is retained unchanged: esbuild, wrangler.jsonc,
celld deploy.
- RPO=0. A write is not acknowledged before it is durable in the bucket. Nodes are replaceable; the bucket is the source of truth.
- One writer per cell. Object-storage compare-and-swap fences ownership, with no consensus service or membership protocol.
- Cheap density. One 8 GB node holds ~1,000 resident cells, about $0.05 per resident cell per month. Inactive cells cost nearly nothing.
- Real Durable Objects. SQLite storage, alarms, hibernatable WebSockets, JS RPC, service bindings, static assets, WebAssembly. See the compatibility matrix.
Run your first fleet in minutes
Install the operator, deploy a Wrangler project to your bucket, and create a WorkerApp. The operator converges the rest.