Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 49 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# OpenClaw on DigitalOcean + Azure
# OpenClaw on DigitalOcean + Azure + GCP

[![Security Checks](https://github.com/PCBZ/OpenClaw_Docker/actions/workflows/security.yml/badge.svg)](https://github.com/PCBZ/OpenClaw_Docker/actions/workflows/security.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Last Commit](https://img.shields.io/github/last-commit/PCBZ/OpenClaw_Docker)](https://github.com/PCBZ/OpenClaw_Docker/commits/main)
[![Terraform](https://img.shields.io/badge/Terraform-%3E%3D1.5-844fba?logo=terraform&logoColor=white)](https://www.terraform.io)
[![DigitalOcean](https://img.shields.io/badge/DigitalOcean-Droplet-0080ff?logo=digitalocean&logoColor=white)](https://www.digitalocean.com)
[![Azure](https://img.shields.io/badge/Azure-VM-0078d4?logo=microsoft-azure&logoColor=white)](https://azure.microsoft.com)
[![GCP](https://img.shields.io/badge/GCP-CloudRun-4285F4?logo=googlecloud&logoColor=white)](https://cloud.google.com/run)
[![OpenRouter](https://img.shields.io/badge/OpenRouter-Free%20Tier-ff6b35?logoColor=white)](https://openrouter.ai)
[![OpenClaw](https://img.shields.io/badge/OpenClaw-2026-00e5cc?logoColor=white)](https://openclaw.bot)
[![Telegram](https://img.shields.io/badge/Telegram-Bot-26a5e4?logo=telegram&logoColor=white)](https://telegram.org)
[![Slack](https://img.shields.io/badge/Slack-Bot-4a154b?logo=slack&logoColor=white)](https://slack.com)

One-command deployment of an [OpenClaw](https://openclaw.bot) AI agent on DigitalOcean or Azure VM with Telegram and Slack support. After `terraform apply`, the bot is fully operational with no manual SSH steps required.
One-command deployment of an [OpenClaw](https://openclaw.bot) AI agent on DigitalOcean, Azure VM, or GCP Cloud Run with Telegram and Slack support. After `terraform apply`, the bot is fully operational with no manual SSH steps required.

## Features

Expand All @@ -28,6 +29,7 @@ One-command deployment of an [OpenClaw](https://openclaw.bot) AI agent on Digita
- SSH key pair
- DigitalOcean account + API token (for DO path)
- Azure subscription + service principal credentials (for Azure path)
- GCP project with Cloud Run + Storage + Secret Manager enabled (for Cloud Run path)
- OpenRouter API key
- Telegram bot token (from [@BotFather](https://t.me/BotFather))
- Slack App-Level token (starts with `xapp-`)
Expand Down Expand Up @@ -96,6 +98,40 @@ swap_size = 2
openclaw_memory_limit_mb = 800
```

#### Option C: GCP Cloud Run

```bash
cd terraform/gcp_cloudrun
cp terraform.tfvars.example terraform.tfvars
```

Edit `terraform.tfvars`:

```hcl
project_id = "your-gcp-project-id"
region = "us-west1"
service_name = "openclaw"

# Leave empty to use Artifact Registry GHCR proxy
container_image = ""
enable_ghcr_proxy = true
ghcr_remote_repository_id = "ghcr-remote"
ghcr_image_path = "openclaw/openclaw"
ghcr_image_tag = "latest"

bucket_name = "your-gcp-project-id-openclaw-state"
min_instances = 1
max_instances = 1
```

Cloud Run deployment in this repo uses:
- `2 vCPU`, `4Gi` memory
- CPU always allocated (`cpu_idle = false`)
- `min_instances = 1`
- Runtime state on local ephemeral storage (`/tmp/openclaw-state`) for startup stability
- Config persistence via mounted GCS bucket (`openclaw.json`, mounted read-only)
- `bonjour` plugin disabled for Cloud Run compatibility (avoids mDNS probing crash loop)

### 3. Load secrets via direnv

```bash
Expand All @@ -116,10 +152,19 @@ Wait ~5 minutes for bootstrap to complete. The bot will start automatically.
### 5. Verify

```bash
terraform output ssh_command # SSH into the server if needed
terraform output cloud_run_url
```

Send a message to your bot on Telegram to confirm it's working.
Then:
- Open the Cloud Run URL to confirm service is reachable.
- Send a Telegram message to confirm bot response.
- (Optional) send a Slack message if Slack tokens are configured.

## Cloud Run Notes

- This Cloud Run setup intentionally does **not** persist full runtime/plugin cache state.
- Only required static/config artifacts are persisted in GCS.
- If you destroy infra but want to keep APIs enabled, this repo sets `disable_on_destroy = false` for required GCP services.

## Switching Models

Expand Down
21 changes: 21 additions & 0 deletions terraform/gcp_cloudrun/.envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Auto-load secrets from .env into Terraform variables
# Requires: brew install direnv && eval "$(direnv hook zsh)" >> ~/.zshrc

dotenv ../../.env

export TF_VAR_project_id="${GCP_PROJECT_ID:-}"
export TF_VAR_gcp_credentials_json="${GCP_CREDENTIALS_JSON:-}"
if [ -n "${GCP_CREDENTIALS_FILE:-}" ]; then
credentials_file_expanded="${GCP_CREDENTIALS_FILE/#\~/$HOME}"
if [ -f "${credentials_file_expanded}" ]; then
export TF_VAR_gcp_credentials_json="$(cat "${credentials_file_expanded}")"
fi
fi

export TF_VAR_openrouter_api_key="$OPENROUTER_API_KEY"
export TF_VAR_telegram_bot_token="$TELEGRAM_BOT_TOKEN"
export TF_VAR_openclaw_gateway_token="$OPENCLAW_GATEWAY_TOKEN"
export TF_VAR_brave_api_key="${BRAVE_API_KEY:-}"
export TF_VAR_telegram_owner_id="${TELEGRAM_OWNER_ID:-}"
export TF_VAR_slack_app_token="${SLACK_APP_TOKEN:-}"
export TF_VAR_slack_bot_token="${SLACK_BOT_TOKEN:-}"
18 changes: 18 additions & 0 deletions terraform/gcp_cloudrun/identity_iam.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
resource "google_service_account" "cloudrun" {
account_id = "${var.service_name}-run-sa"
display_name = "OpenClaw Cloud Run runtime"

depends_on = [google_project_service.required]
}

resource "google_storage_bucket_iam_member" "state_rw" {
bucket = google_storage_bucket.state.name
role = "roles/storage.objectViewer"
member = "serviceAccount:${google_service_account.cloudrun.email}"
}

resource "google_project_iam_member" "secret_accessor" {
project = var.project_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.cloudrun.email}"
}
154 changes: 154 additions & 0 deletions terraform/gcp_cloudrun/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
resource "google_cloud_run_v2_service" "openclaw" {
name = var.service_name
location = var.region
ingress = "INGRESS_TRAFFIC_ALL"
deletion_protection = false

template {
service_account = google_service_account.cloudrun.email
timeout = "3600s"

scaling {
min_instance_count = var.min_instances
max_instance_count = var.max_instances
}

containers {
image = local.effective_container_image
command = ["/bin/sh"]
args = ["-lc", "openclaw gateway run --bind lan --port \"$${PORT:-8080}\" --allow-unconfigured"]

ports {
container_port = 8080
}

# First boot may stage plugin runtime deps on the mounted GCS volume.
# Give startup probe enough budget to avoid false negatives.
startup_probe {
initial_delay_seconds = 5
period_seconds = 10
timeout_seconds = 5
failure_threshold = 60
tcp_socket {
port = 8080
}
}

resources {
limits = {
cpu = "2"
memory = "4Gi"
}
cpu_idle = false
}

volume_mounts {
name = "openclaw-state"
mount_path = "/mnt/openclaw-persist"
}

env {
name = "HOME"
value = "/home/node"
}

env {
name = "OPENCLAW_STATE_DIR"
value = "/tmp/openclaw-state"
}

env {
name = "OPENCLAW_CONFIG_PATH"
value = "/mnt/openclaw-persist/openclaw.json"
}

env {
name = "OPENCLAW_CONFIG_HASH"
value = md5(local.openclaw_json_content)
}

env {
name = "OPENCLAW_GATEWAY_PORT"
value = "8080"
}

env {
name = "OPENROUTER_API_KEY"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.openrouter_api_key.secret_id
version = "latest"
}
}
}

env {
name = "TELEGRAM_BOT_TOKEN"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.telegram_bot_token.secret_id
version = "latest"
}
}
}

env {
name = "OPENCLAW_GATEWAY_TOKEN"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.gateway_token.secret_id
version = "latest"
}
}
}

dynamic "env" {
for_each = var.brave_api_key != "" ? [1] : []
content {
name = "BRAVE_API_KEY"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.brave_api_key[0].secret_id
version = "latest"
}
}
}
}

env {
name = "SLACK_APP_TOKEN"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.slack_app_token.secret_id
version = "latest"
}
}
}

env {
name = "SLACK_BOT_TOKEN"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.slack_bot_token.secret_id
version = "latest"
}
}
}
}

volumes {
name = "openclaw-state"
gcs {
bucket = google_storage_bucket.state.name
read_only = true
}
}
}

depends_on = [
google_artifact_registry_repository_iam_member.ghcr_remote_reader,
google_storage_bucket_object.openclaw_json,
google_storage_bucket_iam_member.state_rw,
google_project_iam_member.secret_accessor
]
}
80 changes: 80 additions & 0 deletions terraform/gcp_cloudrun/openclaw.json.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"gateway": {
"bind": "lan",
"auth": { "mode": "token", "token": "${openclaw_gateway_token}" },
"mode": "local",
"remote": { "token": "${openclaw_gateway_token}" }
},
"agents": {
"defaults": {
"model": {
"primary": "openrouter/openai/gpt-4o-mini",
"fallbacks": [
"openrouter/anthropic/claude-haiku-4.5",
"openrouter/meta-llama/llama-3.3-70b-instruct:free",
"openrouter/auto"
]
},
"models": {
"openrouter/anthropic/claude-opus-4.6": {"alias": "opus"},
"openrouter/anthropic/claude-sonnet-4.6": {"alias": "sonnet"},
"openrouter/anthropic/claude-haiku-4.5": {"alias": "haiku"},
"openrouter/openai/gpt-5.4": {"alias": "gpt5"},
"openrouter/openai/gpt-4o": {"alias": "gpt4o"},
"openrouter/openai/gpt-4o-mini": {"alias": "mini"},
"openrouter/google/gemini-2.5-pro": {"alias": "gemini-pro"},
"openrouter/google/gemini-2.5-flash": {"alias": "flash"},
"openrouter/deepseek/deepseek-r1": {"alias": "r1"},
"openrouter/mistralai/devstral-small": {"alias": "devstral"},
"openrouter/meta-llama/llama-3.3-70b-instruct:free": {"alias": "llama"},
"openrouter/nvidia/nemotron-3-super-120b-a12b:free": {"alias": "nemotron"},
"openrouter/qwen/qwen3-coder:free": {"alias": "coder"},
"openrouter/cognitivecomputations/dolphin-mistral-24b-venice-edition:free": {"alias": "uncensored"},
"openrouter/auto": {"alias": "auto"}
},
"compaction": { "mode": "safeguard", "reserveTokensFloor": 4000 }
}
},
"tools": {
"web": {
"search": { "enabled": true, "provider": "brave" },
"fetch": { "enabled": true }
},
"deny": ["browser"]
},
"plugins": {
"entries": {
"bonjour": { "enabled": false },
"telegram": { "enabled": true },
"slack": { "enabled": true },
"openrouter": { "enabled": true },
"brave": {
"enabled": true,
"config": { "webSearch": { "apiKey": "${brave_api_key}" } }
}
}
},
"channels": {
"telegram": {
"enabled": true,
"allowFrom": ["*"],
"accounts": {
"default": {
"botToken": "${telegram_bot_token}",
"allowFrom": ["*"],
"dmPolicy": "open",
"groupPolicy": "open"
}
}
},
"slack": {
"enabled": true,
"mode": "socket",
"allowFrom": ["*"],
"appToken": "${slack_app_token}",
"botToken": "${slack_bot_token}",
"dmPolicy": "open",
"groupPolicy": "open"
}
}
}
19 changes: 19 additions & 0 deletions terraform/gcp_cloudrun/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
output "cloud_run_service_name" {
description = "Cloud Run service name"
value = google_cloud_run_v2_service.openclaw.name
}

output "cloud_run_url" {
description = "Cloud Run service URL"
value = google_cloud_run_v2_service.openclaw.uri
}

output "state_bucket_name" {
description = "Persistent OpenClaw state bucket"
value = google_storage_bucket.state.name
}

output "runtime_service_account" {
description = "Runtime service account email"
value = google_service_account.cloudrun.email
}
Loading
Loading