1. Introduction
Overview
In this lab, you will deploy a fully persistent, secure instance of OpenClaw—an open-source AI agent framework—to Cloud Run Instances. You will interact with your AI agent directly using OpenClaw's built-in Web UI (with the option to connect messaging channels like Telegram or WhatsApp), back its home workspace with Google Cloud Storage, and manage API credentials securely using Google Cloud Secret Manager.
Before getting started, you can explore the OpenClaw Documentation to familiarize yourself with OpenClaw's architecture, tools, and agent workflows.
What you'll do
- Enable required Google Cloud APIs and create a dedicated service account with required IAM permissions.
- Store API keys and gateway passwords securely in Secret Manager.
- Prepare an
openclaw.jsonconfiguration file with Gemini model settings and gateway UI enabled. - Prepare a Cloud Storage bucket to persist container state.
- (Optional) Upload the Cloud Run Sandbox Provider plugin to enable secure, isolated code execution via
/usr/local/gcp/bin/sandbox. - Deploy OpenClaw using
gcloud beta run instances create. - Interact directly with your OpenClaw AI agent using its built-in Web UI.
- (Optional) Configure a messaging channel (Telegram or WhatsApp).
- (Optional) Extend your agent's capabilities by adding custom Skills to Cloud Storage.
What you'll learn
- How to deploy OpenClaw to Cloud Run Instances with its built-in Control Web UI.
- How to mount Cloud Storage buckets to Cloud Run Instances.
- How to securely inject Secret Manager secrets as environment variables into Cloud Run.
- How to run long-running, persistent agent workloads on Cloud Run Instances.
- How to configure and upload custom agent skills to Cloud Storage.
2. Setup and Requirements
GCP Project Setup
- Sign in to the Google Cloud Console.
- Create or select a Google Cloud Project.
- Ensure billing is enabled for your Google Cloud project.
Open Cloud Shell
Activate Google Cloud Shell from the top toolbar of the Cloud Console.
Install Beta Component & Set Project
Ensure the beta component is installed for gcloud beta run instances:
gcloud components install beta --quiet
export PROJECT_ID=<YOUR_PROJECT_ID>
export REGION="us-west1"
Enable Required Google Cloud APIs
In Cloud Shell, enable the Cloud Run, Secret Manager, Cloud Storage, and Gemini APIs:
gcloud services enable \
run.googleapis.com \
secretmanager.googleapis.com \
storage.googleapis.com \
generativelanguage.googleapis.com \
compute.googleapis.com
3. (Optional) Set Up Messaging Integrations (Telegram or WhatsApp)
You can connect OpenClaw to Telegram or WhatsApp. Choose Option A or Option B below.
Option A: Telegram Bot Setup
- Open Telegram and search for
@BotFather. - Send the
/newbotcommand and follow the prompts to specify a bot name and username. - Copy the generated HTTP API Token (e.g.,
123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ). - Search for
@userinfoboton Telegram, send/start, and copy your numeric User ID (e.g.,8035936176).
Option B: WhatsApp Setup
- Obtain your personal WhatsApp phone number in international format without spaces or symbols (e.g.,
+15551234567). - OpenClaw connects via the WhatsApp channel allowlist policy.
4. Create Dedicated Service Account
To adhere to the principle of least privilege, create a dedicated IAM service account for OpenClaw:
export SERVICE_ACCOUNT_NAME="openclaw-sa"
gcloud iam service-accounts create ${SERVICE_ACCOUNT_NAME} \
--display-name="OpenClaw Service Account"
export SERVICE_ACCOUNT="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
5. Store Credentials in Secret Manager
We will store sensitive API credentials in Google Cloud Secret Manager so Cloud Run can securely inject them into the container at boot time.
1. Obtain and Store Gemini API Key
- Visit Google AI Studio and sign in with your Google account.
- Click Create API key and select your Google Cloud project (
${PROJECT_ID}). - Copy the generated API key.
Store the API key in Secret Manager and grant the service account access:
echo -n "YOUR_GEMINI_API_KEY" | gcloud secrets create gemini-api-key \
--data-file=- \
--replication-policy="automatic"
gcloud secrets add-iam-policy-binding gemini-api-key \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/secretmanager.secretAccessor"
2. Generate and Store Gateway Password
To protect your publicly accessible OpenClaw instance, generate a secure random password and store it in Secret Manager:
export OPENCLAW_GATEWAY_PASSWORD=$(openssl rand -hex 16)
echo "Generated Gateway Password: ${OPENCLAW_GATEWAY_PASSWORD}"
echo -n "${OPENCLAW_GATEWAY_PASSWORD}" | gcloud secrets create openclaw-gateway-password \
--data-file=- \
--replication-policy="automatic"
gcloud secrets add-iam-policy-binding openclaw-gateway-password \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/secretmanager.secretAccessor"
3. (Optional) Create Channel Secret (Telegram or WhatsApp)
- For Telegram:
echo -n "YOUR_TELEGRAM_BOT_TOKEN" | gcloud secrets create telegram-bot-token \ --data-file=- \ --replication-policy="automatic" gcloud secrets add-iam-policy-binding telegram-bot-token \ --member="serviceAccount:${SERVICE_ACCOUNT}" \ --role="roles/secretmanager.secretAccessor" - For WhatsApp:
echo -n "YOUR_WHATSAPP_TOKEN_OR_KEY" | gcloud secrets create whatsapp-token \ --data-file=- \ --replication-policy="automatic" gcloud secrets add-iam-policy-binding whatsapp-token \ --member="serviceAccount:${SERVICE_ACCOUNT}" \ --role="roles/secretmanager.secretAccessor"
6. Prepare Cloud Storage Bucket & openclaw.json Configuration
OpenClaw requires a configuration file named openclaw.json at /home/node/.openclaw/openclaw.json.
- Create a Cloud Storage Bucket & Grant Access:
export BUCKET_NAME="openclaw-state-${PROJECT_ID}" gcloud storage buckets create gs://${BUCKET_NAME} --location=${REGION} gcloud storage buckets add-iam-policy-binding gs://${BUCKET_NAME} \ --member="serviceAccount:${SERVICE_ACCOUNT}" \ --role="roles/storage.objectUser" - Create
openclaw.json: Create a file namedopenclaw.jsonin Cloud Shell. Update thechannelssection to match your chosen channel (Telegram or WhatsApp):{ "gateway": { "mode": "local", "port": 18789, "trustedProxies": ["0.0.0.0/0"], "bind": "lan", "auth": { "password": "${OPENCLAW_GATEWAY_PASSWORD}" }, "controlUi": { "dangerouslyDisableDeviceAuth": true, "allowedOrigins": ["*"], "enabled": true } }, "agents": { "defaults": { "model": { "primary": "google/gemini-3.1-pro-preview" }, "sandbox": { "mode": "all", "workspaceAccess": "rw", "backend": "cloud-run-sandbox" } } }, "channels": { "telegram": { "enabled": true, "defaultAccount": "default", "accounts": { "default": { "enabled": true, "dmPolicy": "allowlist", "allowFrom": [ "YOUR_TELEGRAM_USER_ID" ] } } }, "whatsapp": { "enabled": false, "defaultAccount": "default", "accounts": { "default": { "enabled": false, "dmPolicy": "allowlist", "allowFrom": [ "+15551234567" ] } } } }, "plugins": { "load": { "paths": [ "/home/node/.openclaw/plugins" ] }, "entries": { "google": { "enabled": true }, "telegram": { "enabled": true }, "whatsapp": { "enabled": false }, "cloud-run-sandbox-provider": { "enabled": true } } } } - Upload
openclaw.jsonto Cloud Storage Bucket Root:gcloud storage cp openclaw.json gs://${BUCKET_NAME}/openclaw.json - (Optional) Upload Cloud Run Sandbox Provider Plugin:When
agents.defaults.sandbox.modeis set to"all", OpenClaw by default attempts to spawn a local Docker daemon for sandboxed tool execution. Because standard container images on Cloud Run do not run Docker, you can enable the Cloud Run Sandbox Provider plugin. When deployed with the--sandbox-launcherflag on Cloud Run, the platform automatically injects the native gVisor micro-VM sandbox binary (/usr/local/gcp/bin/sandbox), enabling secure, isolated code execution inside the instance in ~25ms.- Create a local plugin directory:
mkdir -p plugins/cloud-run-sandbox-provider - Create
plugins/cloud-run-sandbox-provider/package.json:{ "name": "cloud-run-sandbox-provider", "version": "1.0.0", "description": "Cloud Run Sandbox Provider for OpenClaw", "type": "module", "main": "index.mjs", "openclaw": { "extensions": [ "./index.mjs" ] } } - Create
plugins/cloud-run-sandbox-provider/openclaw.plugin.json:{ "id": "cloud-run-sandbox-provider", "name": "Cloud Run Sandbox Provider", "entry": "./index.mjs", "kind": "tool", "activation": { "onStartup": true }, "configSchema": { "type": "object", "properties": {} } } - Create
plugins/cloud-run-sandbox-provider/index.mjs:import { spawn } from "node:child_process"; import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { registerSandboxBackend } from "openclaw/plugin-sdk/sandbox"; const SANDBOX_BINARY_PATH = process.env.OPENCLAW_SANDBOX_BINARY_PATH || "/usr/local/gcp/bin/sandbox"; function runCommandRaw(command, args, stdin = null) { return new Promise((resolve) => { const child = spawn(command, args, { env: { ...process.env } }); let stdout = Buffer.alloc(0); let stderr = Buffer.alloc(0); child.stdout.on("data", (data) => { stdout = Buffer.concat([stdout, data]); }); child.stderr.on("data", (data) => { stderr = Buffer.concat([stderr, data]); }); child.on("error", (err) => { resolve({ code: -1, stdout, stderr: Buffer.from(err.message) }); }); child.on("close", (code) => { resolve({ code: code ?? 0, stdout, stderr }); }); if (stdin != null) { child.stdin.write(stdin); } child.stdin.end(); }); } async function ensureSandboxRunning(sandboxId, workspaceDir) { const check = await runCommandRaw(SANDBOX_BINARY_PATH, ["exec", sandboxId, "--", "/bin/true"]); if (check.code === 0) { return; } const runArgs = [ "run", "--detach", "--write", "--mount", `type=bind,source=${workspaceDir},destination=${workspaceDir}`, sandboxId ]; const start = await runCommandRaw(SANDBOX_BINARY_PATH, runArgs); if (start.code !== 0) { throw new Error(`Failed to start Cloud Run sandbox ${sandboxId}: ${start.stderr.toString("utf8")}`); } } class CloudRunSandboxHandle { constructor(params) { this.id = "cloud-run-sandbox"; this.runtimeId = params.sessionKey; this.runtimeLabel = `cloud-run-sandbox:${params.sessionKey}`; this.workdir = params.workspaceDir; this.workspaceDir = params.workspaceDir; } async buildExecSpec(params) { const { command, args, workdir, env, usePty } = params; const positional = ["sh", ...(args || [])]; const execArgs = ["exec"]; if (env) { for (const [key, val] of Object.entries(env)) { execArgs.push("-e", `${key}=${val}`); } } const activeWorkdir = workdir || this.workdir; if (activeWorkdir) { execArgs.push("--workdir", activeWorkdir); } execArgs.push( this.runtimeId, "--", "/bin/sh", "-c", command, ...positional ); return { argv: [SANDBOX_BINARY_PATH, ...execArgs], env: { ...process.env }, stdinMode: usePty ? "pipe-open" : "pipe-closed" }; } async runShellCommand(params) { await ensureSandboxRunning(this.runtimeId, this.workspaceDir); const spec = await this.buildExecSpec({ command: params.script, args: params.args, workdir: params.workdir, env: params.env, usePty: params.usePty, }); return new Promise((resolve, reject) => { const child = spawn(spec.argv[0], spec.argv.slice(1), { env: spec.env }); let stdout = Buffer.alloc(0); let stderr = Buffer.alloc(0); child.stdout.on("data", (data) => { stdout = Buffer.concat([stdout, data]); }); child.stderr.on("data", (data) => { stderr = Buffer.concat([stderr, data]); }); child.on("error", reject); child.on("close", (rawCode) => { const code = rawCode ?? 0; if (code !== 0 && !params.allowFailure) { const stderrStr = stderr.toString("utf8"); const summary = stderrStr.trim().split("\n").slice(-3).join(" | ").slice(0, 400); reject(Object.assign( new Error(`Cloud Run sandbox shell exited with code ${code}: ${summary}`), { code, stdout, stderr }, )); return; } resolve({ code, stdout, stderr }); }); if (params.stdin != null) { child.stdin.write(params.stdin); } child.stdin.end(); }); } } export const cloudRunSandboxManager = { async describeRuntime({ entry }) { const check = await runCommandRaw(SANDBOX_BINARY_PATH, ["exec", entry.containerName, "--", "/bin/true"]); return { running: check.code === 0, actualConfigLabel: "cloud-run-sandbox", configLabelMatch: true, }; }, async removeRuntime({ entry }) { await runCommandRaw(SANDBOX_BINARY_PATH, ["delete", entry.containerName, "--force"]); } }; export default definePluginEntry({ id: "cloud-run-sandbox-provider", name: "Cloud Run Sandbox", kind: "tool", register(api) { registerSandboxBackend("cloud-run-sandbox", { factory: async (params) => { const sanitizedSessionKey = params.sessionKey.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase(); await ensureSandboxRunning(sanitizedSessionKey, params.workspaceDir); return new CloudRunSandboxHandle({ ...params, sessionKey: sanitizedSessionKey }); }, manager: cloudRunSandboxManager }); } }); - Upload the plugin folder to Cloud Storage:
gcloud storage cp -r plugins gs://${BUCKET_NAME}/ - Verify Cloud Storage Bucket Layout:Confirm your bucket structure contains
openclaw.jsonat the root and the plugin files inplugins/cloud-run-sandbox-provider/:gs://${BUCKET_NAME}/ ├── openclaw.json └── plugins/ └── cloud-run-sandbox-provider/ ├── package.json ├── openclaw.plugin.json └── index.mjs
- Create a local plugin directory:
7. Deploy OpenClaw on Cloud Run Instances
Deploy OpenClaw using gcloud beta run instances create:
gcloud beta run instances create openclaw-instance \
--image ghcr.io/openclaw/openclaw:latest \
--service-account ${SERVICE_ACCOUNT} \
--port 18789 \
--cpu 4 \
--memory 4Gi \
--public \
--sandbox-launcher \
--add-volume mount-path=/home/node/.openclaw,type=cloud-storage,mount-options="uid=1000;gid=1000;file-mode=0700;dir-mode=0700",bucket=${BUCKET_NAME} \
--set-secrets GEMINI_API_KEY=gemini-api-key:latest,OPENCLAW_GATEWAY_PASSWORD=openclaw-gateway-password:latest \
--region ${REGION}
Key Parameter Breakdown:
--image ghcr.io/openclaw/openclaw:latest: OpenClaw container image.--service-account ...: Attaches the dedicatedopenclaw-saservice account.--sandbox-launcher: Enables the Cloud Run sandbox launcher, injecting the/usr/local/gcp/bin/sandboxbinary into the container for secure, gVisor-isolated code execution.--add-volume ...: Mounts the Cloud Storage bucket directly to/home/node/.openclaw. Usingfile-mode=0700;dir-mode=0700ensures proper permissions for OpenClaw.--set-secrets ...: Injects credentials directly from Secret Manager into environment variables. (Optional: If you configured Telegram in the optional steps, append,TELEGRAM_BOT_TOKEN=telegram-bot-token:latestto--set-secrets.)--public: Allows public access to the URL.
8. Interact Directly via the OpenClaw Web UI
OpenClaw includes a built-in Control Web UI that allows you to manage and chat with your AI agent directly from your browser:
- Retrieve your Cloud Run Instance URL: In Cloud Shell, run:
gcloud beta run instances describe openclaw-instance \ --region ${REGION} \ --format="value(status.urls[0])" - Access the OpenClaw Control UI:
- Open the output URL in your web browser.
- When prompted for authentication in the OpenClaw Control UI:
- Enter the generated gateway password (
${OPENCLAW_GATEWAY_PASSWORD}) from Secret Manager. - If the login modal displays separate Username and Password fields, leave the username field blank (or enter
admin) and supply${OPENCLAW_GATEWAY_PASSWORD}in the password field.
- Enter the generated gateway password (
- Start Prompting Your Agent:
- Once authenticated, you will see the OpenClaw Control Dashboard.
- You can prompt Gemini directly from the chat interface, view active agent sessions, inspect persistent workspace files, and manage tools!
9. (Optional) Verify Messaging Channel Integrations
If you configured Telegram or WhatsApp in the optional setup steps above, you can verify messaging delivery:
- Check instance logs in Cloud Shell:
gcloud run instances logs read openclaw-instance --region ${REGION} --limit 20 - Open Telegram (or WhatsApp) and send a message (e.g.
/startorHello OpenClaw!). - The bot will authenticate your user ID against the allowlist in
openclaw.jsonand respond using Gemini!
10. (Optional) Extend Your Agent with Skills
OpenClaw supports Skills—modular capability packages that teach your agent specific workflows, specialized CLI tools, and domain-specific instructions.
How Skills Work
Every skill is a directory containing a SKILL.md file. It begins with YAML frontmatter specifying metadata (name and description), followed by Markdown instructions:
---
name: summarize-logs
description: Summarize Cloud Run error logs into actionable bullet points.
---
# Log Summarizer Skill
When asked to analyze or summarize logs:
1. Parse error stack traces and group similar errors by frequency.
2. Identify root causes such as memory limits, timeouts, or permission errors.
3. Propose concrete remediation steps.
name: The skill identifier (also callable directly as a slash command, e.g./summarize-logs).description: Tells OpenClaw's model when to automatically invoke this skill in natural conversation.
Add a Custom Skill to Your Cloud Storage Bucket
Because your Cloud Storage bucket is mounted directly to /home/node/.openclaw, skills placed under gs://${BUCKET_NAME}/skills/ are automatically loaded on boot and persisted across instance restarts:
- Create a Local Skill Directory:
mkdir -p my-skill cat << 'EOF' > my-skill/SKILL.md --- name: summarize-logs description: Summarize Cloud Run error logs into actionable bullet points. --- # Log Summarizer Skill When asked to analyze or summarize logs, group errors by frequency and suggest actionable fixes. EOF - Upload the Skill to Cloud Storage:
gcloud storage cp -r my-skill gs://${BUCKET_NAME}/skills/ - Use the Skill in Chat:
- Return to the OpenClaw Control Web UI.
- You can now prompt your agent using
/summarize-logsor ask natural-language questions matching the skill description!
11. Clean Up
To avoid incurring charges to your Google Cloud account for the resources used in this codelab:
- Delete the Cloud Run Instance:
gcloud beta run instances delete openclaw-instance --region ${REGION} --quiet - Delete Secret Manager Secrets:
gcloud secrets delete gemini-api-key --quiet gcloud secrets delete telegram-bot-token --quiet gcloud secrets delete openclaw-gateway-password --quiet - Delete Cloud Storage Bucket:
gcloud storage rm -r gs://${BUCKET_NAME} - Delete Dedicated Service Account:
gcloud iam service-accounts delete ${SERVICE_ACCOUNT} --quiet
12. Conclusion
Congratulations! You have successfully deployed a secure, fully persistent instance of OpenClaw on Cloud Run Instances backed by Cloud Storage, Secret Manager, and your preferred messaging channel!