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.