Deploy OpenClaw on Cloudflare Containers with Litestream

Run a single OpenClaw instance behind a Cloudflare Worker with a Durable Object and Litestream backups to R2. This guide is for developers with a Cloudflare account and Docker experience.

Read this when

  • You want to run OpenClaw on Cloudflare Containers
  • You are evaluating R2-backed SQLite recovery on ephemeral containers
  • You need to choose between webhook scale-to-zero and always-on channels

Run a single OpenClaw instance behind a Cloudflare Worker paired with a named Durable Object, using the official OpenClaw image and Litestream replication to R2.

Warning

This deployment target is experimental. Litestream safeguards SQLite databases, not the full OpenClaw state directory. Review Limits and recovery before using production credentials.

What you need

  • A Cloudflare account with Workers, Containers, and R2 enabled
  • Docker Buildx with linux/amd64 support
  • A public Docker Hub repository for the derived image
  • Node.js and npm
  • Provider and channel credentials for your OpenClaw setup

The template is located at scripts/cloudflare. It deploys a standard-2 Container with max_instances: 1.

How it works

All HTTP and WebSocket traffic from the Worker is directed to a single, stable Durable Object name. That Durable Object owns one Container instance and acts as the sole-writer barrier around the Litestream replica. The Container exposes OpenClaw on port 8080, and the Durable Object checks /healthz there before forwarding traffic.

flowchart TD
    client[Channels, browsers, API clients]
    worker[Cloudflare Worker]
    durable[Durable Object, one stable name]
    container[Container running the OpenClaw Gateway on 8080]
    litestream[Litestream sidecar process]
    r2[(R2 bucket of SQLite replicas)]

    client --> worker
    worker --> durable
    durable --> container
    container --> litestream
    litestream -- continuous WAL streaming --> r2
    r2 -- restore on boot --> container

Litestream monitors two SQLite roots:

  • /home/node/.openclaw/state/*.sqlite
  • /home/node/.openclaw/agents/**/*.sqlite

At startup, the entrypoint uses R2's S3 ListObjectsV2 API as the restore manifest, filters out paths outside those roots, restores each discovered database, and only then launches the Gateway.

Tested on this template against a real R2 bucket: roughly 2.4 seconds from write to replica, and about 9 seconds to restore both databases into a fresh Container that reached a healthy Gateway approximately 13 seconds after start. Treat these as rough estimates, not firm commitments.

Deploy

Prepare the template

Clone OpenClaw and navigate to the template directory:

git clone https://github.com/openclaw/openclaw.git
cd openclaw/scripts/cloudflare
npm install
npx wrangler login
npx wrangler whoami

Verify that Wrangler selected the correct Cloudflare account before creating any resources.

Create R2 storage

Create the bucket:

npx wrangler r2 bucket create openclaw-backups

In the Cloudflare dashboard, generate an R2 API token with object read/write access scoped to that bucket. Keep the access key ID and secret access key outside the checkout.

In wrangler.jsonc, change <account-id> in the endpoint. If a different bucket name is used, adjust both LITESTREAM_BUCKET and r2_buckets[].bucket_name.

The R2 binding exists for Worker-side access and documentation purposes. Litestream cannot use a Worker binding from inside the Container; it relies on R2's S3 endpoint and credentials passed through Worker secrets.

Publish the Container image

Swap <official-openclaw-image-digest> in Dockerfile with an immutable digest from the official openclaw/openclaw Docker Hub repository.

Build the derived image for Cloudflare's required architecture and push it to a public Docker Hub repository:

docker buildx build \
  --platform linux/amd64 \
  --tag docker.io/<docker-hub-user>/openclaw-cloudflare:<version> \
  --push \
  .
docker buildx imagetools inspect \
  docker.io/<docker-hub-user>/openclaw-cloudflare:<version>

Replace the containers[].image placeholder in wrangler.jsonc with the resulting immutable docker.io/...@sha256:... reference. Cloudflare Containers can pull public Docker Hub images directly; GHCR is not a supported source for this template.

Deploy the Worker and Container

Compile the Worker and deploy it:

npm run check
npm run deploy

The first deployment creates the Worker, the SQLite-backed Durable Object class, the Container application, and the R2 binding.

Set runtime secrets

Add the R2 and Gateway credentials via Wrangler's secret prompt:

npx wrangler secret put LITESTREAM_ACCESS_KEY_ID
npx wrangler secret put LITESTREAM_SECRET_ACCESS_KEY
npx wrangler secret put OPENCLAW_GATEWAY_TOKEN

Add provider and channel variables as needed. For example:

npx wrangler secret put OPENAI_API_KEY
npx wrangler secret put TELEGRAM_BOT_TOKEN

src/container.ts passes an explicit allowlist of environment variables to the Container. Add another name there before using a different environment-backed credential.

Bootstrap OpenClaw

First boot requires one interactive session inside the Container. SSH access is disabled by default; enable it temporarily by adding this to the container entry in wrangler.jsonc, then redeploy:

"ssh": { "enabled": true }

Open the deployed Worker URL once to start the instance. Then locate the application and instance IDs and connect:

npx wrangler containers list
npx wrangler containers instances <application-id> --json
npx wrangler containers ssh <instance-id>

SSH is wrangler-mediated and limited to accounts with container write access. After bootstrap you can remove the ssh block and redeploy; the restored state survives the replacement via Litestream.

Inside the Container, run a SecretRef-based setup. This example uses OpenAI and Telegram:

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 your exact bootstrap recipe in a private, reproducible runbook. A fresh Container disk does not retain the generated config.

Verify the deployment

Run these checks after the first bootstrap, before you depend on this deployment.

Confirm the Gateway answers. /healthz reports that the listener is up. /startupz additionally reports that startup work finished while ignoring channel health, so it stays green when one channel account is broken; it is served only by images built from the release that added it:

curl -sS https://<worker-subdomain>.workers.dev/healthz
curl -sS -H "Authorization: Bearer $OPENCLAW_GATEWAY_TOKEN" \
  https://<worker-subdomain>.workers.dev/readyz

Confirm replication is actually reaching R2. Litestream writes keys under replicas/state/<database>/<generation>/, so list that prefix with any S3-compatible client using the same R2 credentials you gave Litestream. Wrangler cannot list object keys, only fetch them by exact path:

aws s3 ls "s3://openclaw-backups/replicas/" --recursive \
  --endpoint-url "https://<account-id>.r2.cloudflarestorage.com"

The Cloudflare dashboard's R2 object browser shows the same tree. An empty prefix after several minutes of activity means replication is not working; fix it before continuing.

Rehearse recovery before you need it. An untested restore path is not a backup:

  1. Send one message so the Gateway writes a session row.
  2. Wait about ten seconds for replication.
  3. Delete the Container instance, or redeploy to force replacement.
  4. Reopen the Worker URL and confirm the conversation still exists.

If step 4 loses data, stop and fix replication before connecting production channels.

Cost and sizing

Containers require the Workers Paid plan. Memory and disk bill on the resources provisioned for the instance type for as long as the Container is awake; CPU bills on active use only.

The default standard-2 instance provisions 1 vCPU, 6 GiB memory, and 12 GB disk. Running it always-on for a full month is therefore dominated by provisioned memory rather than by how busy the agent is. At the published rates, that is roughly 40 to 50 US dollars per month including the plan fee, mostly memory, before egress.

This matters for the lifecycle decision below:

  • Socket channels keep the Container awake, so they pay the always-on rate. A small always-on virtual machine is often cheaper. Choose Cloudflare here for its operational model, colocation with other Cloudflare services, or the R2 durability path, not to save money.
  • Webhook-only installations sleep, and a sleeping Container bills nothing. That is where this target is genuinely inexpensive.

Verify current rates on Cloudflare's Containers pricing page before committing; these figures are estimates from the published rate card and change independently of OpenClaw.

Observability

Stream Worker and Container logs while reproducing an issue:

npx wrangler tail
npx wrangler containers list
npx wrangler containers instances <application-id> --json

Gateway logs stay inside the Container. Reach them over the temporary SSH session described in the bootstrap step, or forward them to your own collector. The Container filesystem is ephemeral, so treat in-Container logs as debugging output rather than as a durable record.

Choose the lifecycle mode

OPENCLAW_WEBHOOK_ONLY defaults to false, which keeps the Container running through idle periods. Keep this default for channels that maintain sockets or long-lived processes, including:

  • Discord
  • Slack Socket Mode
  • WhatsApp

Set OPENCLAW_WEBHOOK_ONLY to true only when every enabled channel receives traffic through HTTP webhooks. In that mode, the Container stops after ten idle minutes and cold-starts on the next request.

Warning

Scale-to-zero always boots from a blank disk. Turn it on only if some external system can reapply your declarative bootstrap. Litestream can bring back SQLite data, but it cannot rebuild openclaw.json, credential files, installed plugins, or workspaces.

Limits and recovery

  • Single writer: each request maps to the same Durable Object name, and Cloudflare keeps exactly one live Durable Object instance for that name. Do not raise max_instances or add alternate routing that bypasses this constraint. A short-lived overlap of old and new Containers during a platform replacement or rollout is an acceptable experimental tradeoff.
  • Recovery point: the one-second Litestream sync interval usually yields an RPO measured in seconds. This is not synchronous replication, and a hard stop can lose writes that never made it to R2.
  • Ephemeral disk: any sleep, replacement, or host restart begins from the image plus the restored SQLite databases. Rely on full OpenClaw archives for config, credential files, plugin files, and workspaces.
  • Rollback: older database bytes act as time travel. Ratcheting channel credentials, especially WhatsApp, can fall out of sync; approvals and delivery/dedupe state also revert. Reconnect affected channels and check pending approvals before resuming. See Restore.
  • WebSockets: WebSockets are supported through Worker and Container proxying. Cloudflare caps each received WebSocket message at 32 MiB.
  • Egress: outbound traffic goes out through shared Cloudflare IP space. This target offers no fixed egress address.
  • Provider boundary: this is a deployment template, not an OpenClaw cloudWorkers provider. Its operator SSH access does not fulfill that provider's SSH execution contract.

Update

Create a new derived image from a fresh immutable official OpenClaw digest, push it, update the derived digest in wrangler.jsonc, and deploy:

npm run check
npm run deploy

Test updates and rollbacks against a separate R2 bucket first. Keep current state intact before activating older bytes.

Troubleshooting

Worker returns 5xx and the Container never becomes ready -- Cloudflare only runs linux/amd64 images pulled from a public registry. Rebuild with --platform linux/amd64, make sure the derived Docker Hub repository is public, and confirm containers[].image points at the pushed digest rather than a moving tag.

Deployment succeeds but every request times out -- The Container helper waits for GET /healthz. Verify that the Gateway inside the Container listens on port 8080 and that no bootstrap step altered the port.

A probe passes but the Gateway is not actually serving -- The Control UI answers unknown paths with a catch-all 200, so probing a route your image does not serve looks permanently healthy. Check that the response body is JSON, not HTML, before trusting a probe.

Litestream logs authentication or signature errors -- Litestream requires R2 S3 API credentials, which differ from a Cloudflare API token. Create an R2 API token and use its access key ID and secret access key, and confirm LITESTREAM_ENDPOINT holds your account ID.

First boot logs no databases to restore -- Expected on an empty bucket. The entrypoint treats an empty replica listing as a fresh installation and starts the Gateway normally.

/readyz returns 503 while /startupz returns 200 -- Working as designed. Startup finished, and a configured channel account is unhealthy. Inspect channel status rather than restarting the Container; see Health checks.

wrangler containers ssh is rejected -- SSH ships disabled. Add "ssh": { "enabled": true } to the container entry, redeploy, then connect.

Configuration disappeared after a sleep or a redeploy -- Litestream restores SQLite databases only. openclaw.json, credential files, installed plugin files, and workspaces live on the ephemeral disk. Reapply your bootstrap runbook, or keep the installation always-on and take full archives.

Channel sessions break after a restore -- Restoring older bytes rolls back ratcheting credentials. Relink the affected channel and review pending approvals; see Limits and recovery.

WebSocket connections close on large payloads -- Cloudflare closes received WebSocket messages larger than 32 MiB. Reduce attachment sizes or transfer them out of band.

2,019 words · updated Aug 13, 2026