celld-operator

Quick start

From a standard Wrangler project to a converged, routed, autoscaled celld fleet. This walkthrough assumes the operator is installed and you have a qualified bucket (Cloudflare R2 in the examples).

Before you begin

You need the celld CLI and esbuild on your workstation or CI runner to publish deployments (the operator never touches your build):

$ curl -fsSL https://celld.dev/install.sh | sh   # verifiable with gh attestation verify

Worker projects built by celld deploy need esbuild on PATH; asset-only projects do not.

1. Deploy your Worker to the bucket

A standard Wrangler project: wrangler.jsonc (or .json; wrangler.toml is not accepted), module Workers, Durable Object bindings, static assets. Publish it to the fleet's bucket prefix:

$ celld deploy . --bucket s3://platform-cells/apps/chat \
    --endpoint https://ACCOUNT.r2.cloudflarestorage.com --region auto

celld validates the config with a strict key allowlist: an unsupported key (kv_namespaces, routes, triggers, …) stops the deploy with an error naming the key, so nothing fails silently at runtime. Use celld deploy . --dry-run in CI to catch this on pull requests.

2. Provide variables and secrets (optional)

Worker vars and secrets come from a Kubernetes Secret with a vars.env key in NAME=value lines, passed to celld via CELLD_VARS_FILE, never baked into bundles:

$ kubectl create secret generic chat-vars -n tenant-acme \
    --from-literal=vars.env='OPENAI_API_KEY=sk-…'

3. Create the WorkerApp

apiVersion: celld-operator.io/v1alpha1
kind: WorkerApp
metadata:
  name: chat
  namespace: tenant-acme
spec:
  hostnames: ["chat.acme.example.com"]  # routed via the shared Gateway
  appVersion: a3f9c1d2e4b57081          # the Version ID celld deploy printed
  celld:
    image: ghcr.io/denoland/celld:v0.2.0
    updateStrategy: Rolling            # Recreate for non-rolling celld upgrades
  replicas: 3
  bucket:
    name: s3://platform-cells/apps/chat # bucket + per-app prefix
    endpoint: https://ACCOUNT.r2.cloudflarestorage.com
    region: auto
    credentialsFrom:
      iamRole: arn:aws:iam::123456789012:role/celld-chat  # IRSA; or secretRef
  resources:
    memoryGi: 8                        # ~1,000 resident cells per 8 GiB
    maxResidentCells: 1000
  vars:
    secretRef: chat-vars
  websockets: true                     # long idle timeouts, sticky-friendly
  autoscaling:
    enabled: true
    minReplicas: 3
    maxReplicas: 10
    targets:
      residentCellUtilization: 70      # % of maxResidentCells, fleet average
  telemetry:
    enabled: true                      # Parquet traces in the bucket by default
    retention: 30d
$ kubectl apply -f workerapp.yaml

4. Watch it converge

$ kubectl get workerapps -n tenant-acme
NAME   PHASE   APP                READY   RESTORING   AGE
chat   Ready   a3f9c1d2e4b57081   3       0           2m

If something is missing (Gateway API CRDs, KEDA, credentials), the resource stays functional where it can and reports the gap as a condition:

$ kubectl describe workerapp chat -n tenant-acme
…
Conditions:
  Type                     Status   Reason
  Available                True     Ready
  BucketCredentialsReady   True     Configured
  DeployTrackingReady      True     Tracking
  IngressReady             True     RouteReconciled
  MeshPolicyReady          False    IstioUnavailable
  AutoscalingReady         False    KEDAUnavailable

A missing integration is reported, not fatal: here Istio and KEDA are absent, so the NetworkPolicy alone guards the internal listener and spec.autoscaling has no effect — the fleet still serves. Reasons are machine-readable strings; only the False conditions carry an explanatory message.

5. Send a request

$ curl https://chat.acme.example.com/rooms/lobby

No hostnames? Internal-only apps need no ingress at all: in-cluster consumers reach the fleet at chat-celld.tenant-acme.svc:8080.

6. Ship an update

$ celld deploy . --bucket s3://platform-cells/apps/chat …
Current Version ID: 7b21e0c4a9d3f508                              # celld prints this
$ kubectl patch workerapp chat -n tenant-acme --type merge \
    -p '{"spec":{"appVersion":"7b21e0c4a9d3f508"}}'          # roll the fleet

The operator runs a gated rolling update (one pod at a time, stepping only when the whole fleet reports restoring=0), so the rollout is invisible to clients. Prefer celld deploy to be the whole story? Set appVersion: auto. Both flows are covered in Deploying updates.