The template shipped without the operator-facing half: no way to prove the restore path works before depending on it, no cost signal for the always-on versus webhook-only choice, no log surfaces, and no failure-mode index. Adds an architecture diagram, a restore rehearsal, measured recovery numbers from the real-R2 proof, provisioned-resource cost guidance, wrangler tail observability, and a troubleshooting index covering image architecture, Litestream S3 credentials, the startupz/readyz split, and ephemeral-disk config loss.
10 KiB
OpenClaw on Cloudflare Containers (experimental)
This template runs one OpenClaw installation behind a Cloudflare Worker and one named Durable Object. The Durable Object starts a standard-2 Container from a public, digest-pinned Docker Hub image. Litestream continuously replicates the global and per-agent SQLite databases to R2 through its S3-compatible API.
This is an experimental deployment target. Read Operational constraints before using it with real credentials or relying on it for recovery.
Architecture
HTTP/WebSocket request
|
v
Cloudflare Worker
|
v
OpenClawContainer Durable Object (one stable name)
|
v
OpenClaw + Litestream container :8080
|
+--> R2 S3 API (SQLite replicas)
Every HTTP and WebSocket request is forwarded to port 8080. The Container helper checks GET /startupz before admitting traffic. max_instances: 1 and the single Durable Object name are the installation's outer single-writer fence.
Prerequisites
- A Cloudflare account with Workers, Containers, and R2 available
- Docker Buildx with
linux/amd64support - A public Docker Hub repository for the derived image
- Node.js and npm
- Model-provider and channel credentials for the OpenClaw setup you choose
1. Create the R2 bucket and S3 credentials
From this directory:
npm install
npx wrangler login
npx wrangler whoami
npx wrangler r2 bucket create openclaw-backups
In the Cloudflare dashboard, create an R2 API token with object read/write access limited to this bucket. Record its access key ID and secret access key. Do not put either value in this checkout.
Edit wrangler.jsonc:
- replace
<account-id>inLITESTREAM_ENDPOINT - change both
LITESTREAM_BUCKETandr2_buckets[].bucket_nameif you chose another bucket name
The R2 binding is present for Worker-side completeness. Litestream runs inside the Container and cannot consume a Worker binding directly, so it uses R2's S3 endpoint and Worker secrets passed through envVars.
2. Build and publish the image
Choose an immutable, architecture-compatible digest from the official openclaw/openclaw Docker Hub repository. Replace <official-openclaw-image-digest> in Dockerfile, then build and push the derived image:
docker buildx build \
--platform linux/amd64 \
--tag docker.io/<docker-hub-user>/openclaw-cloudflare:<version> \
--push \
.
Make the derived repository public. Resolve its pushed digest, then replace the containers[].image placeholder in wrangler.jsonc:
docker buildx imagetools inspect docker.io/<docker-hub-user>/openclaw-cloudflare:<version>
Use the resulting immutable docker.io/<docker-hub-user>/openclaw-cloudflare@sha256:<digest> reference. Cloudflare Containers can pull public Docker Hub images, but not GHCR images directly.
3. Deploy and set secrets
The first deploy creates the Worker, Durable Object migration, Container application, and R2 binding:
npm run check
npm run deploy
Immediately add the R2 and Gateway secrets. wrangler secret put prompts without writing the value to shell history:
npx wrangler secret put LITESTREAM_ACCESS_KEY_ID
npx wrangler secret put LITESTREAM_SECRET_ACCESS_KEY
npx wrangler secret put OPENCLAW_GATEWAY_TOKEN
Add the provider and channel variables needed by your installation, for example:
npx wrangler secret put OPENAI_API_KEY
npx wrangler secret put TELEGRAM_BOT_TOKEN
src/container.ts passes the listed optional secret names to the Container. Add another explicit name there before using a different environment-backed provider or channel credential.
4. Bootstrap OpenClaw
Open the deployed Worker URL once to start the named instance. Then find the Container application and instance IDs:
npx wrangler containers list
npx wrangler containers instances <application-id> --json
npx wrangler containers ssh <instance-id>
Inside the Container, run the non-interactive SecretRef bootstrap. This example uses OpenAI and Telegram; select the provider and webhook-capable channel that match your secrets:
cd /app
node openclaw.mjs onboard --non-interactive --accept-risk --skip-health \
--mode local \
--auth-choice openai-api-key \
--secret-input-mode ref \
--gateway-auth token \
--gateway-token-ref-env OPENCLAW_GATEWAY_TOKEN \
--skip-channels \
--no-install-daemon
node openclaw.mjs channels add --channel telegram --use-env
node openclaw.mjs doctor --json
Keep the exact bootstrap recipe in a private, reproducible runbook. Litestream does not replicate openclaw.json, credential files, installed plugin files, or workspaces.
5. Verify before relying on it
curl -sS https://<worker-subdomain>.workers.dev/startupz
npx wrangler tail
Then rehearse recovery, because an untested restore path is not a backup: send one message, wait about ten seconds for replication, force a Container replacement, and confirm the conversation survives. Fix replication before connecting production channels if it does not.
Measured on this template against a real R2 bucket: about 2.4 s write-to-replica, about 9 s to restore both databases, and a healthy Gateway about 13 s after a fresh start.
Scale-to-zero policy
The template defaults OPENCLAW_WEBHOOK_ONLY to false. This keeps the Container alive across idle periods for Discord, Slack Socket Mode, WhatsApp, and every other channel that maintains a socket or polling process.
Cost follows directly from that choice. Memory and disk bill on provisioned instance resources for as long as the Container is awake, so an always-on standard-2 is dominated by its 6 GiB of provisioned memory rather than by agent activity -- roughly 40 to 50 US dollars per month at published rates, where a small always-on VM is often cheaper. A sleeping webhook-only Container bills nothing. Check current rates before committing.
Set OPENCLAW_WEBHOOK_ONLY to true only when every enabled channel receives traffic through HTTP webhooks. The Container then stops after ten idle minutes and cold-starts on the next request. Because its disk is fresh after sleep, enable this only when an external process can reapply the declarative bootstrap above; Litestream alone restores SQLite, not the config files needed to activate channels.
Operational constraints
- Experimental: Cloudflare Container lifecycle and rollout behavior can change. Test crash, sleep, rollout, and restore paths with non-production credentials first.
- Single-writer fence: Cloudflare guarantees one live Durable Object instance for a given name, and all Worker requests use the same name. This is the fence around one Litestream replica. A brief old/new Container overlap during replacement or rollout remains an accepted experimental tradeoff; do not raise
max_instancesor route around the named object. - Ephemeral disk: Every Container restart or sleep starts with a fresh filesystem. The entrypoint lists R2 objects, derives the concrete SQLite restore manifest, restores each database, then starts OpenClaw under Litestream.
- Partial durability: Litestream covers
/home/node/.openclaw/state/*.sqliteand recursive per-agent SQLite databases only. Use a separate, privateopenclaw backup createworkflow for config, credential files, plugins, and workspaces. - RPO:
sync-interval: 1snormally yields a seconds-scale recovery point, not zero data loss. Abrupt termination can lose writes that were not uploaded yet. - Rollback is time travel: Restoring older state can desynchronize ratcheting channel credentials (especially WhatsApp), roll back approvals, and roll back delivery/dedupe state. Relink affected channels and review pending approvals before resuming.
- WebSocket limit: Cloudflare accepts received WebSocket messages up to 32 MiB. The Worker/Container proxy supports WebSockets; larger individual messages are closed by the platform.
- Egress identity: outbound traffic comes from shared Cloudflare IP space. Providers that require a fixed source IP need another deployment target or an approved egress design.
- Not a
cloudWorkersprovider: this is a hosting template. Operator SSH access is enabled for bootstrap, but the template does not implement OpenClaw's SSH-based cloud-worker provider contract.
Updating
Build a new derived image from a new immutable official OpenClaw digest, push it, replace the derived digest in wrangler.jsonc, and run:
npm run check
npm run deploy
Treat rollbacks like restores: stop traffic where possible, preserve the current state first, and review credentials, approvals, and delivery state before activating older database bytes.
Troubleshooting
- Container never becomes ready: the image must be
linux/amd64and pulled from a public registry, referenced by digest rather than a moving tag. - Requests time out after a successful deploy: the Container helper waits for
GET /startupzon port8080; confirm the Gateway still binds that port. - Litestream authentication or signature errors: Litestream needs R2 S3 API credentials, not a Cloudflare API token, and
LITESTREAM_ENDPOINTmust contain the account ID. - First boot reports no databases to restore: expected on an empty bucket; the entrypoint treats that as a fresh installation.
/readyzis 503 while/startupzis 200: by design. Startup finished and a channel account is unhealthy; inspect channel status instead of restarting.wrangler containers sshrejected: SSH ships disabled; add"ssh": { "enabled": true }, redeploy, then connect.- Config missing after sleep or redeploy: Litestream restores SQLite only. Reapply the bootstrap runbook or stay always-on and take full archives.
Full operator guide: https://docs.openclaw.ai/install/cloudflare.
Files
wrangler.jsonc: Worker, Durable Object, Container application, and R2 bindingsrc/index.ts: routes all HTTP and WebSocket traffic to one named instancesrc/container.ts: Container port, readiness, environment, and sleep policyDockerfile: official OpenClaw image plus pinned Litestream forlinux/amd64entrypoint.sh: R2 LIST restore discovery, containment checks, and restore-then-exec flowlitestream.yml: watched global and per-agent SQLite directory replicas