DEV Community

Cover image for OpenClaw on GCP Cloud Run: Secure, Serverless, Multi-Tenant
Yaar Naumenko
Yaar Naumenko

Posted on

OpenClaw on GCP Cloud Run: Secure, Serverless, Multi-Tenant

A few days ago, Matias Kreder published a great article on running OpenClaw on AWS Bedrock AgentCore.
The architecture was elegant: ephemeral containers, S3-backed workspace sync, per-user isolation, no always-on VMs.
I was already running OpenClaw on a GKE node, and the bill was… fine, but the node was sitting there 24/7 whether anyone was chatting with the agent or not.

After reading Matias’s post, I thought: GCP has all the same primitives. Can I replicate this pattern natively on GCP?

Turns out yes — and in some ways the GCP path is even cleaner.
Cloud Run v2 supports native GCSFuse volume mounts, which means you get a persistent workspace without a sync daemon, a sidecar, or a background timer.
The filesystem just works across container restarts.
This post walks through how I built a multi-tenant OpenClaw deployment on Cloud Run, with full per-tenant isolation, Telegram/Slack support, and a shared router service as the only public endpoint.
The full repo is on GitHub: openclaw-serverless.

Architecture

GCSFuse Workspace Persistence
Cloud Run containers are ephemeral — they spin up on demand and disappear when idle. OpenClaw stores everything it knows about a user under .openclaw/ (conversation memory, user profiles, tool outputs). Without a persistence strategy, that all disappears the moment a session ends.
The solution here is simpler than the AWS approach: Cloud Run v2 has built-in GCSFuse support. The agent container gets a /data volume mount backed by a per-tenant GCS bucket.
The entrypoint writes openclaw.json to that path on startup, and every file write the agent makes is transparently persisted to GCS. No sync loop, no SIGTERM handler — it just works. Container restarts pick up exactly where the previous session left off.

One intentional detail: config is always overwritten on startup from environment variables. GCSFuse persists agent state; environment variables drive configuration. A new deploy always wins over stale config on disk.

Multi-Tenant Router
Rather than exposing each tenant’s Cloud Run service to the internet, a single lightweight Node.js router sits at the public endpoint. It validates webhook signatures (Telegram secret token, Slack HMAC-SHA256), looks up the tenant by user/channel ID, then forwards the request to the right tenant service using a GCP-issued ID token. Tenant services are deployed with INGRESS_TRAFFIC_INTERNAL_ONLY — they are completely unreachable except through the router.
Webhook secrets are fetched from Secret Manager and cached for 5 minutes. Both channels are fail-closed: requests without valid signatures are rejected before any tenant code runs.

Security

Network: Tenant Cloud Run services have internal-only ingress. The only public endpoint is the router service. Even within GCP, a caller needs a valid ID token to invoke a tenant service — ambient network access is not enough.

Per-tenant isolation: Each tenant gets its own Cloud Run service, GCS bucket, and service account. The tenant SA has objectAdmin on its own bucket only — no IAM binding to any other tenant’s resources. Secrets are scoped per-tenant; the SA can access its own secrets plus the shared Anthropic API key, nothing else.

Least-privilege IAM: The router SA has secretAccessor on webhook secrets and run.invoker on each tenant service. Tenant SAs have secretAccessor on their own secrets and objectAdmin on their own bucket. That’s it.

Secret management: Bot tokens, webhook secrets, and the Anthropic API key all live in Secret Manager. Nothing sensitive in environment variables or container images.

Device pairing bypass: OpenClaw normally requires an interactive shell command to approve devices. Cloud Run has no shell. dmPolicy: allowlist with the tenant’s user ID in allowFrom bypasses pairing entirely — safe because the router already validated the webhook source before the message arrived.

Instructions

Prerequisites

  • GCP project with billing enabled
  • gcloud CLI authenticated
  • terraform / opentofu installed
  • Docker with linux/amd64 build support

1. Clone the repo

git clone https://github.com/cloudon-one/openclaw-serverless
cd openclaw-serverless
Enter fullscreen mode Exit fullscreen mode
  1. Configure your project
# Set your GCP project
export PROJECT_ID=your-gcp-project-id
export REGION=us-central1
export REGISTRY="${REGION}-docker.pkg.dev/${PROJECT_ID}/openclaw"
gcloud config set project $PROJECT_ID
Enter fullscreen mode Exit fullscreen mode

3. Create the Artifact Registry repository and enable APIs

gcloud services enable run.googleapis.com \
  secretmanager.googleapis.com \
  artifactregistry.googleapis.com

gcloud artifacts repositories create openclaw \
  --repository-format=docker \
  --location=$REGION
Enter fullscreen mode Exit fullscreen mode

4. Build and push both container images

./scripts/build.sh
Enter fullscreen mode Exit fullscreen mode

Or manually

gcloud auth configure-docker ${REGION}-docker.pkg.dev

docker build --platform linux/amd64 -t ${REGISTRY}/agent:latest agent/
docker build --platform linux/amd64 -t ${REGISTRY}/router:latest router/
docker push ${REGISTRY}/agent:latest
docker push ${REGISTRY}/router:latest
Enter fullscreen mode Exit fullscreen mode

5. Store your Anthropic API key

echo -n "YOUR_ANTHROPIC_API_KEY" | gcloud secrets create openclaw-anthropic-api-key \
  --data-file=- --replication-policy=automatic
Enter fullscreen mode Exit fullscreen mode

6. Define your first tenant in tenants.yaml

tenants:
  alice:
    display_name: "Alice Smith"
    telegram_user_id: "YOUR_TELEGRAM_USER_ID"
    telegram_enabled: true
    slack_enabled: false
    min_instances: 0
    max_instances: 1
    cpu: "2"
    memory: "2Gi"
Enter fullscreen mode Exit fullscreen mode

7. Deploy infrastructure

cd infrastructure
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars with your project ID, region, registry URL

terraform init
terraform apply
Enter fullscreen mode Exit fullscreen mode

Terraform creates: service accounts, GCS buckets, Secret Manager containers, Cloud Run services (router + one per tenant).
8. Create a Telegram bot

  • Message @botfather on Telegram
  • Use /newbot and copy the token

9. Store tenant secrets

# Telegram bot token
echo -n "YOUR_BOT_TOKEN" | gcloud secrets versions add \
  openclaw-sl-alice-telegram-token --data-file=-

# Webhook validation secret (random)
openssl rand -hex 32 | gcloud secrets versions add \
  openclaw-sl-alice-telegram-webhook-secret --data-file=-
Enter fullscreen mode Exit fullscreen mode

10. Register the Telegram webhook

ROUTER_URL=$(cd infrastructure && terraform output -raw router_url)
WEBHOOK_SECRET=$(gcloud secrets versions access latest \
  --secret=openclaw-sl-alice-telegram-webhook-secret)

curl "https://api.telegram.org/bot${YOUR_BOT_TOKEN}/setWebhook" \
  -d "url=${ROUTER_URL}/webhook/telegram" \
  -d "secret_token=${WEBHOOK_SECRET}"
Enter fullscreen mode Exit fullscreen mode

That’s it. Send a message to your bot on Telegram. The first response takes ~15–20 seconds for a cold start; subsequent messages in the same session are fast.

Conclusion

The solution works well, and the GCSFuse approach is genuinely nicer than S3 sync — one less moving part, no 5-minute flush window, no shutdown race condition.

A few things worth knowing before you deploy:
cpu_idle: false adds cost but is required. Agent sessions involve async operations and WebSocket connections that break under CPU throttling. With min_instances: 0, you’re only paying when the container is actually running, so this is acceptable — but it’s not free.
Gen2 execution environment is non-negotiable. GCSFuse is not available in Gen1. Set execution_environment = “EXECUTION_ENVIRONMENT_GEN2” in Terraform, or the mount will silently fail.

Cold starts are real. First message to an idle tenant takes 15–20 seconds. For async chat, this is fine; for anything latency-sensitive, it’s a problem. Set min_instances: 1 per tenant if you need it — just budget accordingly.

Adding a second tenant is genuinely just one YAML entry and a terraform apply.

The isolation model scales cleanly. Each tenant is a fully independent island with no shared state.
The Terraform state bucket needs to exist before terraform init. Create it manually or bootstrap it separately — classic chicken-and-egg.

Compared to the AWS AgentCore approach, the GCP version skips the NAT gateway entirely (Cloud Run has direct internet egress), which removes the ~$32/month baseline AWS cost.
For a single personal agent, this architecture is essentially free at idle.

Want to try it?
The repo is at https://github.com/cloudon-one/openclaw-serverless.
If you run into issues or want to extend it to other channels (WhatsApp, Discord), the router is straightforward to configure.

Top comments (0)