One-command deployment of an OpenClaw AI agent on DigitalOcean, Azure VM, GCP VM, or GCP Cloud Run with Telegram and Slack support.
- Telegram bot with DM and group chat support
- Slack bot support (Socket Mode)
- Web search via Brave Search (falls back to DuckDuckGo)
- 15+ switchable LLM models via
/model <alias>(GPT-4o, Claude, Gemini, Llama, DeepSeek, and more) - GCP Cloud Run: persistent memory across container restarts via Cloudflare R2 (rclone sidecar syncs every 60s)
- Secrets managed via
.env+ direnv — never committed
- Terraform >= 1.5
- direnv (
brew install direnv) - SSH key pair (for VM targets)
- DigitalOcean account + API token (for DO path)
- Azure subscription + service principal credentials (for Azure path)
- GCP project (for GCP VM or GCP Cloud Run path)
- Cloudflare account + R2 credentials (for GCP Cloud Run path only)
- OpenRouter API key
- Telegram bot token (from @BotFather)
- Slack App-Level token (starts with
xapp-) — optional - Slack Bot User OAuth token (starts with
xoxb-) — optional
cp .env.example .envEdit .env and fill in your values:
| Variable | Description |
|---|---|
OPENROUTER_API_KEY |
From openrouter.ai/keys |
TELEGRAM_BOT_TOKEN |
From @BotFather |
OPENCLAW_GATEWAY_TOKEN |
Any strong random string |
BRAVE_API_KEY |
From api.search.brave.com — optional, falls back to DuckDuckGo |
TELEGRAM_OWNER_ID |
Your Telegram user ID from @userinfobot — grants /model and other privileged commands |
SLACK_APP_TOKEN |
Slack App-Level token (starts with xapp-) — leave empty to disable Slack |
SLACK_BOT_TOKEN |
Slack Bot User OAuth token (starts with xoxb-) — leave empty to disable Slack |
CF_ACCOUNT_ID |
Cloudflare Account ID — Cloud Run only |
CF_API_TOKEN |
Cloudflare API Token with R2:Edit permission — Cloud Run only |
R2_ACCESS_KEY_ID |
R2 S3-compatible Access Key ID — Cloud Run only |
R2_SECRET_ACCESS_KEY |
R2 S3-compatible Secret Access Key — Cloud Run only |
cd terraform/digitalOceanEdit terraform.tfvars to set your DigitalOcean token and optionally adjust region, droplet size, and swap:
do_token = "dop_v1_..."
ssh_public_key_path = "~/.ssh/id_rsa.pub"
region = "tor1" # tor1, sfo3, nyc3, sgp1, ams3, ...
droplet_size = "s-1vcpu-1gb" # $6/mo — increase if OOM
swap_size = "3G"cd terraform/azure_vmCreate terraform.tfvars and set your Azure + VM values:
subscription_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
client_secret = "..."
resource_group_name = "your-existing-rg"
location = "eastus"
ssh_public_key_path = "~/.ssh/id_rsa.pub"
vm_name = "openclaw-b2pts"
vm_size = "Standard_B2pts_v2"
os_disk_size_gb = 30
swap_size = 2
openclaw_memory_limit_mb = 800cd terraform/gcp_vm
cp terraform.tfvars.example terraform.tfvarsEdit terraform.tfvars:
project_id = "your-gcp-project-id"
region = "us-west1"
zone = "us-west1-b"
vm_name = "openclaw-e2-micro"
machine_type = "e2-micro"
boot_disk_size_gb = 30
admin_username = "openclaw"
ssh_public_key_path = "~/.ssh/id_rsa.pub"
network_name = "default"
# Replace with your public IP/CIDR
ssh_allowed_cidrs = ["203.0.113.10/32"]
gateway_allowed_cidrs = ["203.0.113.10/32"]
swap_size = 3
openclaw_memory_limit_mb = 800GCP VM deployment in this repo uses:
- Compute Engine VM (default
e2-micro, Always Free eligible) - 30 GB boot disk
- Swap file + systemd memory cap for OpenClaw process
- Firewall rules for SSH (
22) and OpenClaw gateway (18789) - Shielded VM (Secure Boot + vTPM + Integrity Monitoring)
cd terraform/gcp_cloudrun
cp terraform.tfvars.example terraform.tfvarsEdit terraform.tfvars:
project_id = "your-gcp-project-id"
region = "us-west1"
service_name = "openclaw"
min_instances = 1
max_instances = 3
ghcr_remote_repository_id = "ghcr-remote"
ghcr_image_path = "openclaw/openclaw"
ghcr_image_tag = "latest"
# Cloudflare R2 — persistent memory across container restarts
cloudflare_account_id = "your-cloudflare-account-id"
cloudflare_api_token = "your-cloudflare-api-token"
r2_access_key_id = "your-r2-access-key-id"
r2_secret_access_key = "your-r2-secret-access-key"
r2_bucket_name = "openclaw-state"Cloudflare R2 credentials — two separate tokens are needed:
cloudflare_api_token: dash.cloudflare.com/profile/api-tokens → Create Token → R2:Edit (used by Terraform to create the bucket)r2_access_key_id+r2_secret_access_key: Cloudflare Dashboard → R2 → Manage R2 API Tokens → Create Account API Token (used by rclone at runtime)
GCP Cloud Run deployment in this repo uses:
- Cloud Run service (managed runtime, no VM SSH needed)
- Artifact Registry remote repo proxy for GHCR images
- Secret Manager for all runtime secrets
- Cloudflare R2 for persistent state (session history, memory, soul files) — synced every 60s via rclone sidecar
- Multi-container setup:
openclaw+rclone-syncsidecar sharing an emptyDir volume
# First time only
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc && source ~/.zshrc
direnv allowterraform init # first time only
terraform applyWait ~5 minutes for bootstrap to complete. The bot will start automatically.
For VM targets (DigitalOcean / Azure VM / GCP VM):
terraform output ssh_commandFor GCP Cloud Run target:
terraform output cloud_run_urlThen:
- VM targets: SSH to the VM using the output command.
- Cloud Run target: open the Cloud Run URL from output to confirm the service is reachable.
- Send a Telegram message to confirm bot response.
- (Optional) send a Slack message if Slack tokens are configured.
In Telegram or Slack, use /model <alias>. Available aliases:
| Alias | Model |
|---|---|
opus |
Claude Opus 4 |
sonnet |
Claude Sonnet 4 |
haiku |
Claude Haiku 4 |
gpt4o |
GPT-4o |
mini |
GPT-4o mini |
gemini-pro |
Gemini 2.5 Pro |
flash |
Gemini 2.5 Flash |
r1 |
DeepSeek R1 |
llama |
Llama 3.3 70B (free) |
auto |
OpenRouter auto-select |
ssh_allowed_cidrsandgateway_allowed_cidrsdefault to open (0.0.0.0/0). For production, restrict to your IP interraform.tfvars:
ssh_allowed_cidrs = ["203.0.113.10/32"]
gateway_allowed_cidrs = ["203.0.113.10/32"]- CI security checks are defined in .github/workflows/security.yml (ShellCheck, .envrc policy, Checkov, Gitleaks).
- Secrets are never written to Terraform state — all sensitive variables are injected at runtime via Secret Manager (Cloud Run) or
.env+ direnv.