From ad9324ddbbdb4e9a5f3c5df83a3ea28b712c9a85 Mon Sep 17 00:00:00 2001 From: velrino Date: Thu, 29 Jan 2026 16:19:11 -0300 Subject: [PATCH] feat(deploy): add GCP Compute Engine deployment scripts One-command deployment for Moltbot on GCP Compute Engine: - Automated VM creation, Docker setup, and gateway configuration - Optional Tailscale integration for HTTPS (no SSH tunnel needed) - Optional Telegram allowlist for auto-approved users - Auto-generates secure tokens (gateway token, keyring password) - Container stability checks with auto-restart - Uninstall script included --- docs/platforms/gcp.md | 48 + .../deploy/google/compute-engine/README.md | 576 +++++++++++ scripts/deploy/google/compute-engine/run.sh | 979 ++++++++++++++++++ .../deploy/google/compute-engine/uninstall.sh | 178 ++++ 4 files changed, 1781 insertions(+) create mode 100644 scripts/deploy/google/compute-engine/README.md create mode 100755 scripts/deploy/google/compute-engine/run.sh create mode 100755 scripts/deploy/google/compute-engine/uninstall.sh diff --git a/docs/platforms/gcp.md b/docs/platforms/gcp.md index 6ad3825de..544aea994 100644 --- a/docs/platforms/gcp.md +++ b/docs/platforms/gcp.md @@ -34,8 +34,56 @@ For the generic Docker flow, see [Docker](/install/docker). --- +## Automated deployment (recommended for beginners) + +For a fully automated deployment, use the deployment script. +It handles all the steps below automatically, including Docker setup, gateway configuration, and optional Tailscale HTTPS. + +```bash +# Clone the repository first +git clone https://github.com/moltbot/moltbot.git +cd moltbot + +# Basic deployment (SSH tunnel access) +./scripts/deploy/google/compute-engine/run.sh \ + --project YOUR_PROJECT_ID \ + --anthropic-key sk-ant-xxx + +# With Tailscale for HTTPS access (no SSH tunnel needed) +./scripts/deploy/google/compute-engine/run.sh \ + --project YOUR_PROJECT_ID \ + --anthropic-key sk-ant-xxx \ + --tailscale + +# Full setup: HTTPS + Telegram auto-approval +./scripts/deploy/google/compute-engine/run.sh \ + --project YOUR_PROJECT_ID \ + --anthropic-key sk-ant-xxx \ + --tailscale \ + --telegram-user-id YOUR_TELEGRAM_ID +``` + +**Features:** +- Auto-generates secure tokens (gateway token, keyring password) +- Installs Docker and all dependencies +- Configures `gateway.mode=local` automatically +- Optional Tailscale Serve for HTTPS (avoids browser secure context issues) +- Optional Telegram allowlist (skips manual pairing) +- Container stability checks with auto-restart + +**To uninstall:** +```bash +./scripts/deploy/google/compute-engine/uninstall.sh --project YOUR_PROJECT_ID +``` + +See [scripts/deploy/google/compute-engine/README.md](https://github.com/moltbot/moltbot/blob/main/scripts/deploy/google/compute-engine/README.md) for all options. + +--- + ## Quick path (experienced operators) +If you prefer manual setup or need custom configuration: + 1) Create GCP project + enable Compute Engine API 2) Create Compute Engine VM (e2-small, Debian 12, 20GB) 3) SSH into the VM diff --git a/scripts/deploy/google/compute-engine/README.md b/scripts/deploy/google/compute-engine/README.md new file mode 100644 index 000000000..bc204be86 --- /dev/null +++ b/scripts/deploy/google/compute-engine/README.md @@ -0,0 +1,576 @@ +# Deploy Moltbot to Google Compute Engine + +This script automates the deployment of Moltbot to Google Compute Engine, following the official documentation at [`docs/platforms/gcp.md`](../../../../docs/platforms/gcp.md). + +--- + +## For Beginners: What is all this? + +### Architecture Overview + +```mermaid +graph TB + subgraph You["👤 YOU (user)"] + Laptop["💻 Laptop
(browser)"] + Phone["📱 Phone
(Telegram)"] + Tablet["📱 Tablet
(browser)"] + end + + subgraph GCP["☁️ GOOGLE CLOUD PLATFORM"] + subgraph VM["🖥️ VM (Compute Engine)
Debian 12, 4GB RAM"] + subgraph Docker["🐳 DOCKER"] + Gateway["🤖 MOLTBOT GATEWAY
(port 18789)
─────────────
• Receives Telegram messages
• Processes with Claude AI
• Responds automatically
• Web UI for config"] + end + subgraph Volumes["📁 Mounted Volumes"] + Config["~/.clawdbot/
(config, tokens)"] + Workspace["~/clawd/
(agent workspace)"] + end + end + end + + Laptop -->|"HTTPS
(Tailscale)"| Gateway + Phone -->|"Telegram API"| Gateway + Tablet -->|"HTTPS
(Tailscale)"| Gateway + Gateway --> Config + Gateway --> Workspace +``` + +### What does each technology do? + +| Technology | What it is | Simple explanation | +|------------|-----------|---------------------| +| **Google Cloud Platform (GCP)** | Cloud computing | "Renting a computer in the cloud" - you pay ~$25/mo to have a server running 24/7 | +| **Compute Engine (VM)** | Virtual machine | A virtual computer inside GCP with Debian Linux, 4GB RAM, 20GB disk | +| **Docker** | Container runtime | "Isolated box for applications" - ensures Moltbot works the same everywhere | +| **Tailscale** | Personal VPN | Connects your devices securely, provides free HTTPS, only you can access | +| **Gateway Token** | Authentication | "Moltbot password" - even with the URL, you need the token to enter | +| **Anthropic API (Claude)** | AI service | "The bot's brain" - the AI that generates responses, pay per usage | + +### Message flow in Telegram + +```mermaid +sequenceDiagram + participant You as 👤 You + participant TG as 📱 Telegram + participant GW as 🤖 Moltbot Gateway + participant AI as 🧠 Anthropic API + + You->>TG: 1. "Hello, Claude" + TG->>GW: 2. Forward message + GW->>AI: 3. Send to Claude + AI->>GW: 4. Generate response + GW->>TG: 5. Send reply + TG->>You: 6. "Hello! How can I help?" +``` + +### Why use Tailscale? + +```mermaid +graph LR + subgraph Without["❌ WITHOUT TAILSCALE"] + B1["🌐 Browser"] -->|"HTTP"| IP1["http://35.192.x.x:18789"] + IP1 -->|"❌ BLOCKED"| Error["Insecure connection
WebSocket requires HTTPS"] + end +``` + +```mermaid +graph LR + subgraph With["✅ WITH TAILSCALE"] + B2["🌐 Browser"] -->|"HTTPS"| TS["🔐 Tailscale
Private VPN"] + TS -->|"Encrypted"| VM2["🖥️ VM on GCP"] + VM2 --> OK["✅ Works!
Auto SSL + Private access"] + end +``` + +### Access Modes Compared + +| Mode | Security | Convenience | How it works | +|------|----------|-------------|--------------| +| **SSH Tunnel** (default) | ⭐⭐⭐⭐⭐ | ⭐⭐ | Run SSH command every time, access via `localhost:18789` | +| **Tailscale** (recommended) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Install once, automatic HTTPS, only your devices can access | +| **Public IP** (not recommended) | ⭐⭐ | ⭐⭐⭐⭐ | Anyone can try to access, UI doesn't work (needs HTTPS) | + +### What the script does automatically + +```mermaid +flowchart TD + Start["🚀 run.sh --project X --tailscale --telegram-user-id Y"] --> Step1 + + subgraph Step1["Step 1: Verification"] + V1["✓ gcloud CLI installed?"] + V2["✓ Authenticated on GCP?"] + V3["✓ Project has billing?"] + end + + Step1 --> Step2 + + subgraph Step2["Step 2: Create VM"] + VM1["Create e2-medium (4GB RAM)"] + VM2["Install Docker"] + VM3["Configure firewall"] + end + + Step2 --> Step3 + + subgraph Step3["Step 3: Configure Moltbot"] + C1["Clone repository"] + C2["Create .env with keys"] + C3["Auto-generate tokens"] + end + + Step3 --> Step4 + + subgraph Step4["Step 4: Build & Start"] + B1["docker compose build"] + B2["docker compose up -d"] + B3["Verify stability (3 checks)"] + end + + Step4 --> Step5 + + subgraph Step5["Step 5: Tailscale (if --tailscale)"] + T1["Install Tailscale on VM host"] + T2["Authenticate (link in terminal)"] + T3["Run 'tailscale serve' on host"] + T4["Automatic HTTPS → localhost:18789"] + end + + Step5 --> Step6 + + subgraph Step6["Step 6: Telegram (if --telegram-user-id)"] + TG1["Set dmPolicy = allowlist"] + TG2["Add your user ID to allowlist"] + end + + Step6 --> Result["✅ DONE!
URL: https://moltbot-gateway.tailnet-xxx.ts.net
Telegram: Message @YourBot!"] +``` + +### Estimated Costs + +| Service | Cost | Notes | +|---------|------|-------| +| **GCP VM** (e2-medium) | ~$25/mo | 4GB RAM, runs 24/7 | +| **GCP Disk** (20GB) | ~$2/mo | Persistent storage | +| **GCP Network** | ~$1-5/mo | Depends on usage | +| **Anthropic API** | ~$5-20/mo | Claude 3.5 Sonnet: $3/1M input, $15/1M output | +| **Tailscale** | FREE | Up to 100 devices | +| **Telegram** | FREE | | +| **TOTAL** | **~$33-52/mo** | | + +--- + +## What the Script Does (follows docs/platforms/gcp.md) + +The script automates the deployment steps from the official documentation: + +1. **Checks prerequisites** - gcloud CLI, authentication, billing +2. **Enables APIs** - Compute Engine API +3. **Creates VM** - e2-medium, Debian 12, 20GB (configurable) +4. **Installs Docker** - Via startup script +5. **Clones repository** - `git clone` on the VM +6. **Creates persistent directories** - `~/.clawdbot` and `~/clawd` +7. **Configures .env** - All required variables including `GOG_KEYRING_PASSWORD` +8. **Creates docker-compose.yml** - With volume mounts for persistence +9. **Creates Dockerfile.gcp** - Optimized for GCP deployment +10. **Builds and launches** - `docker compose build && docker compose up -d` +11. **Verifies gateway** - Shows startup logs + +## Prerequisites + +- [Google Cloud CLI](https://cloud.google.com/sdk/docs/install) installed and authenticated +- A Google Cloud project with billing enabled +- An Anthropic API key ([get one here](https://console.anthropic.com/)) + +## Quick Start + +```bash +# Recommended: Full setup with Tailscale HTTPS + Telegram auto-approval +./scripts/deploy/google/compute-engine/run.sh \ + --project YOUR_PROJECT_ID \ + --anthropic-key sk-ant-xxx \ + --tailscale \ + --telegram-user-id YOUR_TELEGRAM_USER_ID + +# Production deployment (e2-medium, SSH tunnel access - most secure) +./scripts/deploy/google/compute-engine/run.sh \ + --project YOUR_PROJECT_ID \ + --anthropic-key sk-ant-xxx + +# Restrict access to your IP only (recommended - auto-detects and confirms) +./scripts/deploy/google/compute-engine/run.sh \ + --project YOUR_PROJECT_ID \ + --anthropic-key sk-ant-xxx \ + --my-ip + +# Free tier deployment (e2-micro, may OOM under load) +./scripts/deploy/google/compute-engine/run.sh \ + --project YOUR_PROJECT_ID \ + --machine-type e2-micro \ + --anthropic-key sk-ant-xxx + +# With .env file +./scripts/deploy/google/compute-engine/run.sh \ + --project YOUR_PROJECT_ID \ + --env-file .env + +# With HTTPS via Tailscale (recommended for direct browser access) +./scripts/deploy/google/compute-engine/run.sh \ + --project YOUR_PROJECT_ID \ + --anthropic-key sk-ant-xxx \ + --tailscale +``` + +To find your Telegram user ID, message [@userinfobot](https://t.me/userinfobot) on Telegram. + +To find your IP address: + +```bash +curl -4 ifconfig.me +``` + +## Options + +| Option | Default | Description | +|--------|---------|-------------| +| `--project PROJECT_ID` | (required) | Google Cloud project ID | +| `--zone ZONE` | `us-central1-a` | Compute Engine zone | +| `--instance NAME` | `moltbot-gateway` | Instance name | +| `--machine-type TYPE` | `e2-medium` | Machine type (4GB RAM, needed for builds) | +| `--disk-size SIZE` | `20GB` | Boot disk size | +| `--env-file FILE` | - | Load environment variables from file | +| `--anthropic-key KEY` | - | Anthropic API key | +| `--gateway-token TOKEN` | auto-generated | Gateway authentication token | +| `--my-ip` | - | Auto-detect your IP and restrict firewall (API only, UI needs HTTPS) | +| `--allowed-ip IP` | - | Restrict firewall to specific IP (API only, UI needs HTTPS) | +| `--tailscale` | - | Install Tailscale for HTTPS access (recommended for UI) | +| `--telegram-user-id ID` | - | Telegram user ID to auto-approve (no pairing needed) | + +## Access Modes + +### SSH Tunnel (default, most secure) + +By default, the gateway binds to loopback only (`127.0.0.1:18789`). Access via SSH tunnel: + +```bash +# Create tunnel from your laptop +gcloud compute ssh moltbot-gateway --zone=us-central1-a --project=YOUR_PROJECT -- -L 18789:127.0.0.1:18789 + +# Open in browser +http://127.0.0.1:18789/ +``` + +**Pros:** Most secure, no ports exposed to internet +**Cons:** Requires SSH tunnel every time you access the UI + +### Tailscale (recommended for direct browser access) + +With `--tailscale` flag, the script: +1. Installs Tailscale on the VM host (before Docker build) +2. Authenticates Tailscale (you click the link in terminal) +3. Runs `sudo tailscale serve --bg --yes 18789` on the host +4. Docker container binds to `0.0.0.0:18789` for Tailscale to proxy + +```bash +./scripts/deploy/google/compute-engine/run.sh \ + --project YOUR_PROJECT \ + --anthropic-key sk-ant-xxx \ + --tailscale + +# Follow the authentication link that appears in the terminal +# Then access via: https://moltbot-gateway.your-tailnet.ts.net/ +``` + +After deployment, you can access the Gateway UI directly via the Tailscale URL - no port number needed (Tailscale Serve handles HTTPS on port 443). + +**Pros:** HTTPS works, direct browser access, secure, no SSH tunnel needed +**Cons:** Requires Tailscale account (free) + +### IP Restricted Access (API only - UI requires HTTPS) + +With `--my-ip` flag, the script auto-detects your IP and asks for confirmation. + +**Note:** Due to browser security (secure context), the UI only works via localhost or HTTPS. Use SSH tunnel or Tailscale for UI access. + +```bash +# Deploy with auto-detected IP restriction (recommended) +./scripts/deploy/google/compute-engine/run.sh \ + --project YOUR_PROJECT \ + --anthropic-key sk-ant-xxx \ + --my-ip + +# The script will: +# 1. Detect your IP (e.g., 186.220.38.207) +# 2. Ask: "Restrict access to IP 186.220.38.207? [Y/n]" +# 3. If yes, proceed with deployment restricted to your IP + +# Or specify IP manually: +./scripts/deploy/google/compute-engine/run.sh \ + --project YOUR_PROJECT \ + --anthropic-key sk-ant-xxx \ + --allowed-ip 186.220.38.207 + +# Access directly (no tunnel needed) +http://INSTANCE_IP:18789/?token=YOUR_GATEWAY_TOKEN +``` + +**Pros:** Direct access to UI without SSH tunnel, secure (only your IP can connect) +**Cons:** If your IP changes, you need to update the firewall rule: + +```bash +# Update allowed IP +gcloud compute firewall-rules update moltbot-gateway \ + --project=YOUR_PROJECT \ + --source-ranges="NEW_IP/32" +``` + +### Public Access (not recommended) + +With `--public` flag, the gateway is exposed to all IPs: + +```bash +# Deploy with public access (open to everyone) +./scripts/deploy/google/compute-engine/run.sh --project YOUR_PROJECT --anthropic-key sk-ant-xxx --public + +# Access directly +http://INSTANCE_IP:18789/?token=YOUR_GATEWAY_TOKEN +``` + +**Warning:** Anyone with the token can access your gateway. Only use this if you understand the security implications. + +### How to Find Your IP Address + +```bash +# Use curl +curl -4 ifconfig.me +``` + +This will return something like `186.220.38.207`. + +**Tip:** Use `--my-ip` flag during deployment to auto-detect and confirm your IP automatically. + +### Managing Allowed IPs + +#### Add or change your IP + +If your IP changes, update the firewall rule: + +```bash +# Update to new single IP +gcloud compute firewall-rules update moltbot-gateway \ + --project=YOUR_PROJECT \ + --source-ranges="NEW_IP/32" +``` + +#### Allow multiple IPs + +To allow multiple IPs (e.g., home and office): + +```bash +# Allow multiple IPs (comma-separated) +gcloud compute firewall-rules update moltbot-gateway \ + --project=YOUR_PROJECT \ + --source-ranges="186.220.38.207/32,200.100.50.25/32" +``` + +#### Allow an IP range + +To allow a range of IPs: + +```bash +# Allow a /24 subnet (e.g., 186.220.38.0 - 186.220.38.255) +gcloud compute firewall-rules update moltbot-gateway \ + --project=YOUR_PROJECT \ + --source-ranges="186.220.38.0/24" +``` + +#### Check current allowed IPs + +```bash +gcloud compute firewall-rules describe moltbot-gateway \ + --project=YOUR_PROJECT \ + --format="value(sourceRanges)" +``` + +## What Persists Where + +| Component | Location | Persistence | Notes | +|-----------|----------|-------------|-------| +| Gateway config | `~/.clawdbot/` | Host volume | `moltbot.json`, tokens | +| OAuth tokens | `~/.clawdbot/` | Host volume | Model auth profiles | +| WhatsApp session | `~/.clawdbot/` | Host volume | Preserves QR login | +| Gmail keyring | `~/.clawdbot/` | Host volume | Requires `GOG_KEYRING_PASSWORD` | +| Agent workspace | `~/clawd/` | Host volume | Code and artifacts | +| Application | Docker image | Rebuilt on update | Node.js app and dependencies | + +## Machine Types + +| Type | vCPU | Memory | Monthly Cost | Notes | +|------|------|--------|--------------|-------| +| e2-micro | 0.25 | 1 GB | Free tier | ❌ OOM during build and runtime | +| e2-small | 0.5 | 2 GB | ~$12/mo | ❌ OOM during Docker build | +| e2-medium | 1 | 4 GB | ~$25/mo | ✅ **Default** - works for build and runtime | + +### Why e2-medium? + +The Docker build process (`pnpm install`) requires ~3GB of RAM. Smaller machines will be killed by the OS (OOM - Out of Memory) during the build phase. + +- **e2-micro (1GB)**: Will OOM during build and often during runtime +- **e2-small (2GB)**: Will OOM during Docker build (`pnpm install`) +- **e2-medium (4GB)**: Works for build and runtime ✅ + +After the initial build, runtime memory usage is lower (~1-2GB). If cost is a concern, you can: +1. Build with `e2-medium` +2. After successful deployment, downgrade to `e2-small` for runtime + +## Post-Deployment Commands + +### SSH to instance + +```bash +gcloud compute ssh moltbot-gateway --zone=us-central1-a --project=YOUR_PROJECT +``` + +### View logs + +```bash +gcloud compute ssh moltbot-gateway --zone=us-central1-a --project=YOUR_PROJECT \ + --command='cd ~/moltbot && docker compose logs -f' +``` + +### Restart gateway + +```bash +gcloud compute ssh moltbot-gateway --zone=us-central1-a --project=YOUR_PROJECT \ + --command='cd ~/moltbot && docker compose restart' +``` + +### Update Moltbot + +```bash +gcloud compute ssh moltbot-gateway --zone=us-central1-a --project=YOUR_PROJECT \ + --command='cd ~/moltbot && git pull && docker compose build && docker compose up -d' +``` + +## Telegram Auto-Approval + +By default, Moltbot requires users to "pair" before using the bot via Telegram. With `--telegram-user-id`, you can pre-approve your Telegram account: + +```bash +./scripts/deploy/google/compute-engine/run.sh \ + --project YOUR_PROJECT \ + --anthropic-key sk-ant-xxx \ + --telegram-user-id 123456789 +``` + +This sets: +- `channels.telegram.dmPolicy: "allowlist"` - Only allowlisted users can message the bot +- `channels.telegram.allowFrom: ["123456789"]` - Your user ID is pre-approved + +**How to find your Telegram user ID:** +1. Message [@userinfobot](https://t.me/userinfobot) on Telegram +2. It will reply with your user ID (a number like `123456789`) + +## Uninstall + +```bash +# Delete instance and firewall rule +./scripts/deploy/google/compute-engine/uninstall.sh --project YOUR_PROJECT + +# Keep firewall for future deployments +./scripts/deploy/google/compute-engine/uninstall.sh --project YOUR_PROJECT --keep-firewall +``` + +## Troubleshooting + +### SSH connection refused + +SSH key propagation can take 1-2 minutes after VM creation. Wait and retry. + +### Instance not starting + +Check the serial console output: + +```bash +gcloud compute instances get-serial-port-output moltbot-gateway \ + --zone=us-central1-a --project=YOUR_PROJECT +``` + +### Out of memory (OOM) + +If you see `exit code: 137` or `Killed` during Docker build, the VM ran out of memory. + +**Solution:** Upgrade to `e2-medium` (4GB RAM): + +```bash +# Stop the VM +gcloud compute instances stop moltbot-gateway --zone=us-central1-a --project=YOUR_PROJECT + +# Change machine type to e2-medium (4GB RAM) +gcloud compute instances set-machine-type moltbot-gateway \ + --zone=us-central1-a --project=YOUR_PROJECT \ + --machine-type=e2-medium + +# Start the VM +gcloud compute instances start moltbot-gateway --zone=us-central1-a --project=YOUR_PROJECT + +# Re-run the build +gcloud compute ssh moltbot-gateway --zone=us-central1-a --project=YOUR_PROJECT \ + --command='cd ~/moltbot && docker compose build && docker compose up -d' +``` + +**Note:** The default is now `e2-medium` to avoid this issue. See [Machine Types](#machine-types) for details. + +### Docker build fails + +SSH into the instance and check disk space: + +```bash +gcloud compute ssh moltbot-gateway --zone=us-central1-a --project=YOUR_PROJECT +df -h +docker system prune -a # Clean up old images +``` + +## Security Recommendations + +1. **Use `--tailscale`** (recommended) - Automatic HTTPS, private access via your tailnet +2. **Use SSH tunnel** - Most secure, but requires tunnel for each access +3. **Use `--allowed-ip`** - Restricts to your IP only (API only, UI needs HTTPS) +4. **Avoid `--public`** - Open to all IPs, only use if you understand the risks +5. **Rotate tokens** - Change `CLAWDBOT_GATEWAY_TOKEN` periodically +6. **Use `--telegram-user-id`** - Pre-approve only your Telegram account (no open pairing) + +## Tailscale HTTPS Details + +When using `--tailscale`, the script sets up automatic HTTPS via Tailscale Serve: + +1. **VM Host**: Tailscale daemon runs on the VM, authenticated to your tailnet +2. **Tailscale Serve**: The script runs `sudo tailscale serve --bg --yes 18789` on the HOST +3. **Docker Container**: Binds to `0.0.0.0:18789` so Tailscale can proxy to it + +The architecture is: +``` +Internet → Tailscale (HTTPS:443) → localhost:18789 → Docker (gateway) +``` + +Tailscale Serve runs on the HOST (not in the container) because it requires root permissions. + +### Manual Tailscale Setup (if not using `--tailscale`) + +If you deployed without `--tailscale` and want to add it later: + +```bash +# SSH to instance +gcloud compute ssh moltbot-gateway --zone=us-central1-a --project=YOUR_PROJECT + +# Install Tailscale on the VM host +curl -fsSL https://tailscale.com/install.sh | sh +sudo tailscale up --hostname=moltbot-gateway + +# Configure Tailscale Serve to proxy HTTPS to the gateway port +sudo tailscale serve --bg --yes 18789 + +# Verify it's working +tailscale serve status +``` + +This gives you a secure URL like `https://moltbot-gateway.your-tailnet.ts.net`. diff --git a/scripts/deploy/google/compute-engine/run.sh b/scripts/deploy/google/compute-engine/run.sh new file mode 100755 index 000000000..7e2208deb --- /dev/null +++ b/scripts/deploy/google/compute-engine/run.sh @@ -0,0 +1,979 @@ +#!/usr/bin/env bash +# +# Deploy Moltbot to Google Compute Engine (follows docs/platforms/gcp.md) +# +# Usage: +# ./scripts/deploy/google/compute-engine/run.sh [OPTIONS] +# +# Options: +# --project PROJECT_ID Google Cloud project ID (required) +# --zone ZONE Compute Engine zone (default: us-central1-a) +# --instance NAME Instance name (default: moltbot-gateway) +# --machine-type TYPE Machine type (default: e2-medium, 4GB RAM for builds) +# --disk-size SIZE Boot disk size (default: 20GB) +# --env-file FILE Upload .env file to the instance +# --anthropic-key KEY Anthropic API key +# --gateway-token TOKEN Gateway token (auto-generated if not provided) +# --public Expose gateway publicly to all IPs (not recommended) +# --allowed-ip IP Expose gateway but restrict to specific IP +# --my-ip Auto-detect your IP, confirm, and restrict access to it (recommended) +# --tailscale Install Tailscale for HTTPS access (recommended) +# --telegram-token TOKEN Telegram bot token (from @BotFather) +# --telegram-user-id ID Your Telegram user ID (auto-approves you, skips pairing) +# --help Show this help message +# +# Examples: +# # Production deployment (e2-small, SSH tunnel access - most secure) +# ./scripts/deploy/google/compute-engine/run.sh --project my-project --anthropic-key sk-ant-xxx +# +# # Restrict access to your IP only (recommended - auto-detects and confirms) +# ./scripts/deploy/google/compute-engine/run.sh --project my-project --anthropic-key sk-ant-xxx --my-ip +# +# # Free tier deployment (e2-micro, may OOM under load) +# ./scripts/deploy/google/compute-engine/run.sh --project my-project --machine-type e2-micro --anthropic-key sk-ant-xxx +# +# # With .env file +# ./scripts/deploy/google/compute-engine/run.sh --project my-project --env-file .env +# +# Prerequisites: +# - gcloud CLI installed and authenticated +# - Google Cloud project with billing enabled +# +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Default values (following docs/platforms/gcp.md) +ZONE="us-central1-a" +INSTANCE_NAME="moltbot-gateway" +MACHINE_TYPE="e2-medium" +DISK_SIZE="20GB" +PROJECT_ID="" +ENV_FILE="" +ARG_ANTHROPIC_KEY="" +ARG_GATEWAY_TOKEN="" +GENERATED_GATEWAY_TOKEN="" +GENERATED_GOG_PASSWORD="" +PUBLIC_ACCESS=false +ALLOWED_IP="" +AUTO_DETECT_IP=false +INSTALL_TAILSCALE=false +TELEGRAM_BOT_TOKEN="" +TELEGRAM_USER_ID="" + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Logging functions +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[OK]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } +log_step() { echo -e "\n${CYAN}━━━ $1 ━━━${NC}"; } +log_detail() { echo -e " ${NC}↳ $1"; } + +show_help() { + head -38 "$0" | tail -36 | sed 's/^#//' | sed 's/^ //' + exit 0 +} + +detect_my_ip() { + echo "" + log_info "Detecting your public IP address..." + local my_ip + my_ip=$(curl -4 -s ifconfig.me 2>/dev/null || curl -4 -s ipv4.icanhazip.com 2>/dev/null || echo "") + if [[ -n "$my_ip" ]]; then + echo "" + echo " Your IPv4 address: $my_ip" + echo "" + ALLOWED_IP="$my_ip" + PUBLIC_ACCESS=true + else + log_error "Could not determine your IP address." + log_detail "Try manually with: --allowed-ip YOUR_IP" + exit 1 + fi +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --project) PROJECT_ID="$2"; shift 2 ;; + --zone) ZONE="$2"; shift 2 ;; + --instance) INSTANCE_NAME="$2"; shift 2 ;; + --machine-type) MACHINE_TYPE="$2"; shift 2 ;; + --disk-size) DISK_SIZE="$2"; shift 2 ;; + --env-file) ENV_FILE="$2"; shift 2 ;; + --anthropic-key) ARG_ANTHROPIC_KEY="$2"; shift 2 ;; + --gateway-token) ARG_GATEWAY_TOKEN="$2"; shift 2 ;; + --public) PUBLIC_ACCESS=true; shift ;; + --allowed-ip) ALLOWED_IP="$2"; PUBLIC_ACCESS=true; shift 2 ;; + --my-ip) AUTO_DETECT_IP=true; shift ;; + --tailscale) INSTALL_TAILSCALE=true; shift ;; + --telegram-token) TELEGRAM_BOT_TOKEN="$2"; shift 2 ;; + --telegram-user-id) TELEGRAM_USER_ID="$2"; shift 2 ;; + --help|-h) show_help ;; + *) log_error "Unknown option: $1"; exit 1 ;; + esac +done + +# Validate required arguments +if [[ -z "$PROJECT_ID" ]]; then + log_error "Project ID is required. Use --project PROJECT_ID" + echo "" + echo "Usage: $0 --project YOUR_PROJECT_ID [OPTIONS]" + exit 1 +fi + +# Auto-detect IP if --my-ip is set +if [[ "$AUTO_DETECT_IP" == "true" ]]; then + detect_my_ip + echo "" + read -p "Restrict access to IP $ALLOWED_IP? [Y/n] " -n 1 -r + echo "" + if [[ $REPLY =~ ^[Nn]$ ]]; then + log_error "Aborted. Use --allowed-ip IP to specify a different IP." + exit 1 + fi + log_success "Will restrict access to: $ALLOWED_IP" +fi + +# Check prerequisites +check_prerequisites() { + log_step "Checking prerequisites" + + log_info "Checking if gcloud CLI is installed..." + if ! command -v gcloud &> /dev/null; then + log_error "gcloud CLI is not installed!" + log_detail "Install from: https://cloud.google.com/sdk/docs/install" + exit 1 + fi + log_detail "gcloud CLI found: $(which gcloud)" + + log_info "Checking gcloud authentication..." + if ! gcloud auth print-access-token &> /dev/null; then + log_error "Not authenticated with gcloud!" + log_detail "Run: gcloud auth login" + exit 1 + fi + log_detail "Authenticated as: $(gcloud config get-value account 2>/dev/null)" + + log_info "Checking billing status..." + local billing_enabled + billing_enabled=$(gcloud billing projects describe "$PROJECT_ID" --format="value(billingEnabled)" 2>/dev/null || echo "false") + if [[ "$billing_enabled" != "True" ]]; then + log_error "Billing is not enabled for project: $PROJECT_ID" + log_detail "Enable billing at: https://console.cloud.google.com/billing/linkedaccount?project=$PROJECT_ID" + exit 1 + fi + log_detail "Billing is enabled" + + log_success "Prerequisites OK" +} + +# Enable required APIs +enable_apis() { + log_step "Enabling required APIs" + + log_info "Enabling Compute Engine API..." + gcloud services enable compute.googleapis.com --project="$PROJECT_ID" 2>&1 || true + log_success "Compute Engine API enabled" +} + +# Create or get instance +create_instance() { + log_step "Setting up Compute Engine instance" + + # Check if instance already exists + log_info "Checking if instance exists..." + if gcloud compute instances describe "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" &>/dev/null; then + log_warn "Instance already exists: $INSTANCE_NAME" + + # Check current machine type and upgrade if needed + local current_type + current_type=$(gcloud compute instances describe "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" --format='value(machineType)' | sed 's|.*/||') + log_detail "Current machine type: $current_type" + + # Upgrade if machine type is too small (e2-micro or e2-small cause OOM) + if [[ "$current_type" == "e2-micro" || "$current_type" == "e2-small" ]]; then + log_warn "Machine type $current_type has insufficient memory for builds (OOM risk)" + log_info "Upgrading to $MACHINE_TYPE (4GB RAM)..." + + # Stop the instance + log_detail "Stopping instance..." + if ! gcloud compute instances stop "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" --quiet; then + log_error "Failed to stop instance for upgrade" + exit 1 + fi + + # Change machine type + log_detail "Changing machine type to $MACHINE_TYPE..." + if ! gcloud compute instances set-machine-type "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" --machine-type="$MACHINE_TYPE"; then + log_error "Failed to change machine type" + exit 1 + fi + + # Start the instance + log_detail "Starting instance..." + if ! gcloud compute instances start "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE"; then + log_error "Failed to start instance" + exit 1 + fi + + log_success "Upgraded to $MACHINE_TYPE" + + # Wait for instance to be ready + log_info "Waiting for instance to be ready..." + sleep 30 + else + log_detail "Machine type OK: $current_type" + fi + + return 0 + fi + + log_info "Creating new instance: $INSTANCE_NAME" + log_detail "Zone: $ZONE" + log_detail "Machine type: $MACHINE_TYPE" + log_detail "Disk size: $DISK_SIZE" + log_detail "Image: Debian 12" + + if gcloud compute instances create "$INSTANCE_NAME" \ + --project="$PROJECT_ID" \ + --zone="$ZONE" \ + --machine-type="$MACHINE_TYPE" \ + --image-family=debian-12 \ + --image-project=debian-cloud \ + --boot-disk-size="$DISK_SIZE" \ + --boot-disk-type=pd-standard \ + --tags=moltbot-server \ + --metadata=startup-script='#!/bin/bash +# Install Docker (following docs/platforms/gcp.md step 5) +apt-get update +apt-get install -y git curl ca-certificates +curl -fsSL https://get.docker.com | sh +usermod -aG docker $(logname 2>/dev/null || echo "") + +echo "Docker installation complete" +'; then + log_success "Instance created: $INSTANCE_NAME" + else + log_error "Failed to create instance" + exit 1 + fi + + # Wait for instance to be ready + log_info "Waiting for instance to be ready..." + sleep 30 + + # Wait for Docker to be installed + log_info "Waiting for Docker installation..." + local max_attempts=30 + local attempt=0 + while [[ $attempt -lt $max_attempts ]]; do + if gcloud compute ssh "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" --command="which docker" &>/dev/null; then + log_success "Docker installed on instance" + break + fi + attempt=$((attempt + 1)) + log_detail "Waiting... ($attempt/$max_attempts)" + sleep 10 + done + + if [[ $attempt -ge $max_attempts ]]; then + log_warn "Startup script may still be running. Will continue anyway." + fi +} + +# Create firewall rule (only if --public flag is used) +setup_firewall() { + if [[ "$PUBLIC_ACCESS" != "true" ]]; then + log_step "Firewall setup (loopback mode)" + log_info "Gateway will be bound to loopback only" + log_detail "Access via SSH tunnel: gcloud compute ssh $INSTANCE_NAME --zone=$ZONE --project=$PROJECT_ID -- -L 18789:127.0.0.1:18789" + + # Delete existing firewall rule if it exists (security: don't leave port open) + if gcloud compute firewall-rules describe moltbot-gateway --project="$PROJECT_ID" &>/dev/null; then + log_info "Removing existing firewall rule (not needed in loopback mode)..." + gcloud compute firewall-rules delete moltbot-gateway --project="$PROJECT_ID" --quiet 2>/dev/null || true + log_detail "Firewall rule removed" + fi + return + fi + + # Determine source ranges + local source_ranges="0.0.0.0/0" + local access_mode="public access mode - open to all IPs" + if [[ -n "$ALLOWED_IP" ]]; then + source_ranges="${ALLOWED_IP}/32" + access_mode="restricted access mode - only $ALLOWED_IP" + fi + + log_step "Setting up firewall ($access_mode)" + + log_info "Checking firewall rule..." + if gcloud compute firewall-rules describe moltbot-gateway --project="$PROJECT_ID" &>/dev/null; then + log_detail "Firewall rule already exists, updating source ranges..." + if gcloud compute firewall-rules update moltbot-gateway \ + --project="$PROJECT_ID" \ + --source-ranges="$source_ranges"; then + log_success "Firewall rule updated with source: $source_ranges" + else + log_error "Failed to update firewall rule" + exit 1 + fi + + # Verify the update worked + local current_ranges + current_ranges=$(gcloud compute firewall-rules describe moltbot-gateway --project="$PROJECT_ID" --format="value(sourceRanges)" 2>/dev/null || echo "") + if [[ "$current_ranges" != *"$source_ranges"* ]] && [[ "$source_ranges" != "0.0.0.0/0" ]]; then + log_warn "Firewall may not have updated correctly. Current: $current_ranges, Expected: $source_ranges" + fi + else + log_info "Creating firewall rule for port 18789..." + if gcloud compute firewall-rules create moltbot-gateway \ + --project="$PROJECT_ID" \ + --allow=tcp:18789 \ + --source-ranges="$source_ranges" \ + --target-tags=moltbot-server \ + --description="Allow Moltbot gateway traffic"; then + log_success "Firewall rule created with source: $source_ranges" + else + log_error "Failed to create firewall rule" + exit 1 + fi + fi +} + +# Configure Moltbot on the instance (following docs/platforms/gcp.md) +configure_moltbot() { + log_step "Configuring Moltbot (following docs/platforms/gcp.md)" + + # Resolve tokens + local anthropic_key="" + local gateway_token="" + local gog_keyring_password="" + + # Get Anthropic key + if [[ -n "$ARG_ANTHROPIC_KEY" ]]; then + anthropic_key="$ARG_ANTHROPIC_KEY" + elif [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then + anthropic_key="$ANTHROPIC_API_KEY" + elif [[ -n "$ENV_FILE" ]] && [[ -f "$ENV_FILE" ]]; then + anthropic_key=$(grep "^ANTHROPIC_API_KEY=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2- || true) + fi + + if [[ -z "$anthropic_key" ]]; then + log_error "Anthropic API key is required!" + log_detail "Provide via --anthropic-key, ANTHROPIC_API_KEY env var, or in .env file" + exit 1 + fi + log_detail "Anthropic API key: ${anthropic_key:0:10}..." + + # Get or generate gateway token + if [[ -n "$ARG_GATEWAY_TOKEN" ]]; then + gateway_token="$ARG_GATEWAY_TOKEN" + elif [[ -n "${CLAWDBOT_GATEWAY_TOKEN:-}" ]]; then + gateway_token="$CLAWDBOT_GATEWAY_TOKEN" + elif [[ -n "$ENV_FILE" ]] && [[ -f "$ENV_FILE" ]]; then + gateway_token=$(grep "^CLAWDBOT_GATEWAY_TOKEN=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2- || true) + fi + + if [[ -z "$gateway_token" ]]; then + log_info "Generating gateway token..." + gateway_token=$(openssl rand -hex 32) + GENERATED_GATEWAY_TOKEN="$gateway_token" + log_detail "Token generated: ${gateway_token:0:16}..." + else + log_detail "Gateway token: ${gateway_token:0:16}..." + fi + + # Get or generate GOG keyring password + if [[ -n "$ENV_FILE" ]] && [[ -f "$ENV_FILE" ]]; then + gog_keyring_password=$(grep "^GOG_KEYRING_PASSWORD=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2- || true) + fi + + if [[ -z "$gog_keyring_password" ]]; then + log_info "Generating GOG keyring password..." + gog_keyring_password=$(openssl rand -hex 32) + GENERATED_GOG_PASSWORD="$gog_keyring_password" + log_detail "GOG password generated" + fi + + # Determine bind mode + local bind_mode="lan" + local port_binding="127.0.0.1:18789:18789" + if [[ "$PUBLIC_ACCESS" == "true" ]] || [[ "$INSTALL_TAILSCALE" == "true" ]]; then + # Tailscale needs access to the gateway, so bind to all interfaces + bind_mode="lan" + port_binding="18789:18789" + fi + + # Get remote username + local remote_user + remote_user=$(gcloud compute ssh "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" --command="whoami" 2>/dev/null || echo "") + if [[ -z "$remote_user" ]]; then + log_error "Could not determine remote username" + exit 1 + fi + log_detail "Remote user: $remote_user" + + # Build extra env vars from file (two formats: .env and docker-compose) + local extra_env_vars="" + local extra_env_file="" + if [[ -n "$ENV_FILE" ]] && [[ -f "$ENV_FILE" ]]; then + log_info "Reading additional variables from $ENV_FILE..." + while IFS='=' read -r key value || [[ -n "$key" ]]; do + # Skip comments and empty lines + [[ -z "$key" || "$key" =~ ^# ]] && continue + # Skip already handled keys + [[ "$key" == "ANTHROPIC_API_KEY" || "$key" == "CLAWDBOT_GATEWAY_TOKEN" || "$key" == "GOG_KEYRING_PASSWORD" ]] && continue + # Add to docker-compose format + extra_env_vars="${extra_env_vars} - ${key}=${value}"$'\n' + # Add to .env file format + extra_env_file="${extra_env_file}${key}=${value}"$'\n' + log_detail "Added: $key" + done < "$ENV_FILE" + fi + + # Add Telegram bot token if provided via command line + if [[ -n "$TELEGRAM_BOT_TOKEN" ]]; then + extra_env_vars="${extra_env_vars} - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}"$'\n' + extra_env_file="${extra_env_file}TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}"$'\n' + log_detail "Added: TELEGRAM_BOT_TOKEN" + fi + + # Build Tailscale volume mount if enabled + local tailscale_volume="" + if [[ "$INSTALL_TAILSCALE" == "true" ]]; then + tailscale_volume=" - /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock"$'\n' + fi + + # Create remote setup script (following docs/platforms/gcp.md steps 6-11) + log_info "Setting up Moltbot on instance..." + + local setup_script='#!/bin/bash +set -e + +REMOTE_USER="'"$remote_user"'" +HOME_DIR="/home/$REMOTE_USER" +INSTALL_TAILSCALE="'"$INSTALL_TAILSCALE"'" + +echo "=== Step 6: Create persistent host directories ===" +mkdir -p "$HOME_DIR/.clawdbot" +mkdir -p "$HOME_DIR/clawd" +chown -R "$REMOTE_USER:$REMOTE_USER" "$HOME_DIR/.clawdbot" +chown -R "$REMOTE_USER:$REMOTE_USER" "$HOME_DIR/clawd" +echo "Created ~/.clawdbot and ~/clawd" + +echo "=== Step 7: Clone Moltbot repository ===" +cd "$HOME_DIR" +if [[ -d "moltbot" ]]; then + echo "Repository already exists, pulling latest..." + cd moltbot + git pull || true +else + git clone https://github.com/moltbot/moltbot.git + cd moltbot +fi +chown -R "$REMOTE_USER:$REMOTE_USER" "$HOME_DIR/moltbot" + +echo "=== Step 8: Configure environment variables ===" +cat > "$HOME_DIR/moltbot/.env" << EOF +# Moltbot GCP Deployment Configuration +# Generated by deploy script (following docs/platforms/gcp.md) + +CLAWDBOT_IMAGE=moltbot:latest +CLAWDBOT_GATEWAY_TOKEN='"$gateway_token"' +CLAWDBOT_GATEWAY_BIND='"$bind_mode"' +CLAWDBOT_GATEWAY_PORT=18789 + +CLAWDBOT_CONFIG_DIR=$HOME_DIR/.clawdbot +CLAWDBOT_WORKSPACE_DIR=$HOME_DIR/clawd + +GOG_KEYRING_PASSWORD='"$gog_keyring_password"' +XDG_CONFIG_HOME=/home/node/.clawdbot + +ANTHROPIC_API_KEY='"$anthropic_key"' +NODE_ENV=production + +# Extra variables from --env-file or --telegram-token +'"$extra_env_file"' +EOF +chown "$REMOTE_USER:$REMOTE_USER" "$HOME_DIR/moltbot/.env" +chmod 600 "$HOME_DIR/moltbot/.env" +echo "Created .env file" + +echo "=== Step 9: Create docker-compose.yml ===" +cat > "$HOME_DIR/moltbot/docker-compose.yml" << EOF +services: + moltbot-gateway: + image: \${CLAWDBOT_IMAGE} + build: + context: . + dockerfile: Dockerfile.gcp + restart: unless-stopped + env_file: + - .env + environment: + - HOME=/home/node + - NODE_ENV=production + - TERM=xterm-256color + - CLAWDBOT_GATEWAY_BIND=\${CLAWDBOT_GATEWAY_BIND} + - CLAWDBOT_GATEWAY_PORT=\${CLAWDBOT_GATEWAY_PORT} + - CLAWDBOT_GATEWAY_TOKEN=\${CLAWDBOT_GATEWAY_TOKEN} + - GOG_KEYRING_PASSWORD=\${GOG_KEYRING_PASSWORD} + - XDG_CONFIG_HOME=\${XDG_CONFIG_HOME} + - ANTHROPIC_API_KEY=\${ANTHROPIC_API_KEY} + - PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +'"$extra_env_vars"' + volumes: + - \${CLAWDBOT_CONFIG_DIR}:/home/node/.clawdbot + - \${CLAWDBOT_WORKSPACE_DIR}:/home/node/clawd +'"$tailscale_volume"' ports: + - "'"$port_binding"'" + command: + [ + "node", + "dist/index.js", + "gateway", + "--bind", + "\${CLAWDBOT_GATEWAY_BIND}", + "--port", + "\${CLAWDBOT_GATEWAY_PORT}" + ] +EOF +chown "$REMOTE_USER:$REMOTE_USER" "$HOME_DIR/moltbot/docker-compose.yml" +echo "Created docker-compose.yml" + +echo "=== Step 10: Create Dockerfile.gcp ===" + +# Build Tailscale installation command if enabled +TAILSCALE_INSTALL="" +if [[ "$INSTALL_TAILSCALE" == "true" ]]; then + TAILSCALE_INSTALL="# Install Tailscale CLI (for tailscale serve) +RUN curl -fsSL https://tailscale.com/install.sh | sh" +fi + +cat > "$HOME_DIR/moltbot/Dockerfile.gcp" << EOF +FROM node:22-bookworm + +# Install system dependencies +RUN apt-get update && apt-get install -y socat curl && rm -rf /var/lib/apt/lists/* + +$TAILSCALE_INSTALL + +# Install Bun (required for build scripts) +RUN curl -fsSL https://bun.sh/install | bash +ENV PATH="/root/.bun/bin:\${PATH}" + +# Optional: Add external binaries here if needed for specific skills +# Example (uncomment and adjust URL if binary exists): +# RUN curl -L https://example.com/binary.tar.gz | tar -xz -C /usr/local/bin + +WORKDIR /app + +# Copy package files first for better caching +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ +COPY ui/package.json ./ui/package.json +COPY patches ./patches +COPY scripts ./scripts + +RUN corepack enable +RUN pnpm install --frozen-lockfile + +COPY . . +RUN CLAWDBOT_A2UI_SKIP_MISSING=1 pnpm build +RUN pnpm ui:install +RUN pnpm ui:build + +ENV NODE_ENV=production + +# Security: Run as non-root user +USER node + +CMD ["node", "dist/index.js"] +EOF +chown "$REMOTE_USER:$REMOTE_USER" "$HOME_DIR/moltbot/Dockerfile.gcp" +echo "Created Dockerfile.gcp" + +echo "=== Step 11: Pre-configure gateway ===" + +# Write config.json directly to the volume BEFORE starting the container +# This prevents the "Missing config" error that causes container restart loops +echo "Writing gateway configuration..." + +# Build the config JSON +CONFIG_JSON="{ + \"gateway\": { + \"mode\": \"local\", + \"auth\": { + \"mode\": \"token\" + }, + \"controlUi\": { + \"allowInsecureAuth\": true + } + }" + +# Add Telegram config if token is present +if grep -q "TELEGRAM_BOT_TOKEN" "$HOME_DIR/moltbot/.env" 2>/dev/null; then + # Check if we have a user ID for allowlist + if [[ "'"$TELEGRAM_USER_ID"'" != "''" ]]; then + CONFIG_JSON="$CONFIG_JSON, + \"channels\": { + \"telegram\": { + \"enabled\": true, + \"dmPolicy\": \"allowlist\", + \"allowFrom\": [\"'"$TELEGRAM_USER_ID"'\"] + } + }" + echo "Telegram configured with allowlist for user '"$TELEGRAM_USER_ID"'" + else + CONFIG_JSON="$CONFIG_JSON, + \"channels\": { + \"telegram\": { + \"enabled\": true + } + }" + echo "Telegram enabled (pairing required)" + fi +fi + +CONFIG_JSON="$CONFIG_JSON +}" + +# Write config to the mounted volume +echo "$CONFIG_JSON" > "$HOME_DIR/.clawdbot/config.json" +chown "$REMOTE_USER:$REMOTE_USER" "$HOME_DIR/.clawdbot/config.json" +chmod 600 "$HOME_DIR/.clawdbot/config.json" +echo "Config written to ~/.clawdbot/config.json" + +echo "=== Step 12: Build and launch ===" +cd "$HOME_DIR/moltbot" + +# Ensure user can run docker +usermod -aG docker "$REMOTE_USER" 2>/dev/null || true + +# Build the image +echo "Building Docker image (this may take several minutes)..." +docker compose build + +# Start the gateway (config already exists, so it will start successfully) +echo "Starting Moltbot gateway..." +docker compose up -d --force-recreate moltbot-gateway + +# Wait for gateway to be ready +echo "Waiting for gateway to be ready..." +for i in {1..30}; do + if curl -sf http://localhost:18789/health >/dev/null 2>&1; then + echo "Gateway is ready!" + break + fi + echo " Waiting... ($i/30)" + sleep 2 +done + +echo "" +echo "=== Step 13: Verify Gateway ===" +sleep 5 +docker compose logs --tail=20 moltbot-gateway + +echo "" +echo "Setup complete!" +' + + # Run setup script on instance + echo "$setup_script" | gcloud compute ssh "$INSTANCE_NAME" \ + --project="$PROJECT_ID" \ + --zone="$ZONE" \ + --command="sudo bash" + + log_success "Moltbot configured and started" + + # Verify service is running and stays running + log_info "Verifying gateway status (checking stability)..." + local check_attempts=0 + local max_checks=6 + local stable_count=0 + local required_stable=3 + + while [[ $check_attempts -lt $max_checks ]]; do + sleep 5 + check_attempts=$((check_attempts + 1)) + + local container_state + container_state=$(gcloud compute ssh "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" \ + --command="cd ~/moltbot && docker compose ps --format '{{.State}}' moltbot-gateway 2>/dev/null" 2>/dev/null || echo "") + + if [[ "$container_state" == "running" ]]; then + stable_count=$((stable_count + 1)) + log_detail "Container running ($stable_count/$required_stable checks)..." + if [[ $stable_count -ge $required_stable ]]; then + log_success "Moltbot gateway is running and stable" + return 0 + fi + else + stable_count=0 + log_warn "Container not running (state: ${container_state:-unknown}), attempt $check_attempts/$max_checks" + + # Try to restart if container stopped + if [[ $check_attempts -lt $max_checks ]]; then + log_info "Attempting to restart container..." + gcloud compute ssh "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" \ + --command="cd ~/moltbot && docker compose up -d moltbot-gateway" 2>/dev/null || true + fi + fi + done + + # If we get here, container is unstable + log_error "Gateway container is not stable after $max_checks attempts" + log_detail "Check logs with:" + log_detail "gcloud compute ssh $INSTANCE_NAME --zone=$ZONE --project=$PROJECT_ID --command='cd ~/moltbot && docker compose logs --tail=50 moltbot-gateway'" + exit 1 +} + +# Install and configure Tailscale for HTTPS access +install_tailscale() { + if [[ "$INSTALL_TAILSCALE" != "true" ]]; then + return + fi + + log_step "Installing Tailscale for HTTPS access" + + log_info "Waiting for apt lock to be released (startup script may still be running)..." + gcloud compute ssh "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" \ + --command='for i in {1..30}; do sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 || break; echo "Waiting for apt lock... ($i/30)"; sleep 5; done' 2>&1 + + log_info "Installing Tailscale and jq on instance..." + gcloud compute ssh "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" \ + --command='sudo apt-get update && sudo apt-get install -y jq && curl -fsSL https://tailscale.com/install.sh | sh' 2>&1 || { + log_warn "First attempt failed, retrying after 10s..." + sleep 10 + gcloud compute ssh "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" \ + --command='sudo apt-get update && sudo apt-get install -y jq && curl -fsSL https://tailscale.com/install.sh | sh' 2>&1 || true + } + + log_info "Starting Tailscale (follow the auth link)..." + echo "" + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ TAILSCALE AUTHENTICATION REQUIRED ║" + echo "║ Click the link below to authenticate: ║" + echo "╚════════════════════════════════════════════════════════════╝" + echo "" + + gcloud compute ssh "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" \ + --command='sudo tailscale up --hostname=moltbot-gateway' 2>&1 + + # Get Tailscale hostname (try jq first, fallback to grep) + TAILSCALE_HOSTNAME=$(gcloud compute ssh "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" \ + --command='tailscale status --json 2>/dev/null | jq -r ".Self.DNSName // empty" 2>/dev/null | sed "s/\.$//"' 2>/dev/null || echo "") + + if [[ -z "$TAILSCALE_HOSTNAME" ]]; then + log_warn "Could not get Tailscale hostname automatically" + log_detail "Get it with: tailscale status" + return + fi + + log_success "Tailscale connected: $TAILSCALE_HOSTNAME" + + # Configure Tailscale Serve on HOST (must run with sudo on host, not from container) + # Container cannot run tailscale serve due to permission issues + log_info "Configuring Tailscale Serve to proxy HTTPS to port 18789..." + gcloud compute ssh "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" \ + --command='sudo tailscale serve --bg --yes 18789' 2>&1 || { + log_warn "Could not configure Tailscale Serve automatically" + log_detail "Run manually on instance: sudo tailscale serve --bg --yes 18789" + } + + log_success "Tailscale Serve configured" + log_success "Access URL: https://$TAILSCALE_HOSTNAME/" +} + +# Get instance external IP +get_instance_ip() { + gcloud compute instances describe "$INSTANCE_NAME" \ + --project="$PROJECT_ID" \ + --zone="$ZONE" \ + --format="value(networkInterfaces[0].accessConfigs[0].natIP)" +} + +# Main +main() { + echo "" + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ Moltbot Compute Engine Deployment ║" + echo "║ (following docs/platforms/gcp.md) ║" + echo "╚════════════════════════════════════════════════════════════╝" + echo "" + echo "Configuration:" + echo " Project: $PROJECT_ID" + echo " Zone: $ZONE" + echo " Instance: $INSTANCE_NAME" + echo " Machine type: $MACHINE_TYPE" + echo " Disk size: $DISK_SIZE" + if [[ "$PUBLIC_ACCESS" == "true" ]]; then + if [[ -n "$ALLOWED_IP" ]]; then + echo " Access mode: Restricted to IP $ALLOWED_IP" + else + echo " Access mode: Public (open to all IPs)" + fi + else + echo " Access mode: Loopback (SSH tunnel)" + fi + [[ "$INSTALL_TAILSCALE" == "true" ]] && echo " Tailscale: Yes (HTTPS)" + [[ -n "$TELEGRAM_USER_ID" ]] && echo " Telegram user: $TELEGRAM_USER_ID (auto-approved)" + [[ -n "$ENV_FILE" ]] && echo " Env file: $ENV_FILE" + echo "" + + check_prerequisites + enable_apis + create_instance + setup_firewall + install_tailscale # Must run BEFORE Docker so Tailscale socket exists + configure_moltbot + + local instance_ip + instance_ip=$(get_instance_ip) + + echo "" + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ Deployment Complete! ║" + echo "╚════════════════════════════════════════════════════════════╝" + echo "" + log_success "Instance IP: $instance_ip" + echo "" + + # Show generated secrets if we created them + if [[ -n "${GENERATED_GATEWAY_TOKEN:-}" ]] || [[ -n "${GENERATED_GOG_PASSWORD:-}" ]]; then + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ IMPORTANT: Save these generated secrets! ║" + echo "╚════════════════════════════════════════════════════════════╝" + echo "" + if [[ -n "${GENERATED_GATEWAY_TOKEN:-}" ]]; then + echo " CLAWDBOT_GATEWAY_TOKEN=$GENERATED_GATEWAY_TOKEN" + fi + if [[ -n "${GENERATED_GOG_PASSWORD:-}" ]]; then + echo " GOG_KEYRING_PASSWORD=$GENERATED_GOG_PASSWORD" + fi + echo "" + echo " Save these securely - you'll need the gateway token to connect." + echo "" + fi + + # Access instructions based on mode + if [[ "$INSTALL_TAILSCALE" == "true" ]] && [[ -n "${TAILSCALE_HOSTNAME:-}" ]]; then + echo "Access (Tailscale HTTPS - secure, private network):" + echo "" + echo " Prerequisites:" + echo " 1. Install Tailscale on your device: https://tailscale.com/download" + echo " 2. Log in with the same account used during deployment" + echo "" + echo " Open dashboard (copy this URL):" + if [[ -n "${GENERATED_GATEWAY_TOKEN:-}" ]]; then + echo " https://$TAILSCALE_HOSTNAME/?token=$GENERATED_GATEWAY_TOKEN" + else + echo " https://$TAILSCALE_HOSTNAME/?token=YOUR_GATEWAY_TOKEN" + fi + echo "" + echo " Security: Only devices on YOUR Tailnet can access this URL." + echo " Even with the URL+token, others cannot connect." + elif [[ "$PUBLIC_ACCESS" == "true" ]]; then + echo "Access (public mode - WARNING: exposed to internet):" + echo "" + echo " API/CLI access:" + echo " http://$instance_ip:18789" + echo "" + echo " Test health:" + echo " curl http://$instance_ip:18789/health" + echo "" + echo " Browser dashboard (requires SSH tunnel for WebSocket):" + echo " gcloud compute ssh $INSTANCE_NAME --zone=$ZONE --project=$PROJECT_ID -- -L 18789:127.0.0.1:18789" + echo " Then open: http://127.0.0.1:18789/" + echo "" + if [[ -n "${GENERATED_GATEWAY_TOKEN:-}" ]]; then + echo " Token: $GENERATED_GATEWAY_TOKEN" + fi + echo "" + echo " WARNING: HTTP over public IP does not work in browser dashboard." + echo " Browser requires HTTPS or localhost. Use --tailscale for HTTPS." + else + echo "Access (SSH tunnel mode - recommended for security):" + echo "" + echo " Step 1: Create SSH tunnel from your laptop:" + echo " gcloud compute ssh $INSTANCE_NAME --zone=$ZONE --project=$PROJECT_ID -- -L 18789:127.0.0.1:18789" + echo "" + echo " Step 2: Open in browser:" + echo " http://127.0.0.1:18789/" + echo "" + echo " Step 3: Enter your gateway token" + if [[ -n "${GENERATED_GATEWAY_TOKEN:-}" ]]; then + echo " Token: $GENERATED_GATEWAY_TOKEN" + fi + fi + + # Show Telegram info if configured + if [[ -n "$TELEGRAM_USER_ID" ]]; then + echo "" + echo "Telegram:" + echo " Your user ID ($TELEGRAM_USER_ID) is pre-approved." + echo " Just message your bot - no pairing needed!" + fi + + echo "" + echo "Useful commands:" + echo "" + echo " SSH to instance:" + echo " gcloud compute ssh $INSTANCE_NAME --zone=$ZONE --project=$PROJECT_ID" + echo "" + echo " View logs:" + echo " gcloud compute ssh $INSTANCE_NAME --zone=$ZONE --project=$PROJECT_ID --command='cd ~/moltbot && docker compose logs -f'" + echo "" + echo " Restart gateway:" + echo " gcloud compute ssh $INSTANCE_NAME --zone=$ZONE --project=$PROJECT_ID --command='cd ~/moltbot && docker compose restart'" + echo "" + echo " Update Moltbot:" + echo " gcloud compute ssh $INSTANCE_NAME --zone=$ZONE --project=$PROJECT_ID --command='cd ~/moltbot && git pull && docker compose build && docker compose up -d'" + echo "" + echo "What persists where:" + echo " ~/.clawdbot → Gateway config, OAuth tokens, WhatsApp session" + echo " ~/clawd → Agent workspace, code artifacts" + echo " Docker image → Application and dependencies" + echo "" + echo "Cloud Console:" + echo " https://console.cloud.google.com/compute/instancesDetail/zones/$ZONE/instances/$INSTANCE_NAME?project=$PROJECT_ID" + echo "" + + # Show quick access URL at the very end for easy copy/click + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ 🚀 QUICK ACCESS - Copy and paste this URL: ║" + echo "╚════════════════════════════════════════════════════════════╝" + echo "" + if [[ "$INSTALL_TAILSCALE" == "true" ]] && [[ -n "${TAILSCALE_HOSTNAME:-}" ]]; then + if [[ -n "${GENERATED_GATEWAY_TOKEN:-}" ]]; then + echo " https://${TAILSCALE_HOSTNAME}/?token=${GENERATED_GATEWAY_TOKEN}" + else + echo " https://${TAILSCALE_HOSTNAME}/" + fi + echo "" + echo " (Requires Tailscale installed on your device)" + else + echo " Step 1: Run this command in a NEW terminal:" + echo " gcloud compute ssh $INSTANCE_NAME --zone=$ZONE --project=$PROJECT_ID -- -L 18789:127.0.0.1:18789" + echo "" + echo " Step 2: Open this URL in your browser:" + if [[ -n "${GENERATED_GATEWAY_TOKEN:-}" ]]; then + echo " http://127.0.0.1:18789/?token=${GENERATED_GATEWAY_TOKEN}" + else + echo " http://127.0.0.1:18789/" + fi + fi + echo "" +} + +main diff --git a/scripts/deploy/google/compute-engine/uninstall.sh b/scripts/deploy/google/compute-engine/uninstall.sh new file mode 100755 index 000000000..d3db62f62 --- /dev/null +++ b/scripts/deploy/google/compute-engine/uninstall.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +# +# Uninstall Moltbot from Google Compute Engine +# +# Usage: +# ./scripts/deploy/google/compute-engine/uninstall.sh [OPTIONS] +# +# Options: +# --project PROJECT_ID Google Cloud project ID (required) +# --zone ZONE Compute Engine zone (default: us-central1-a) +# --instance NAME Instance name (default: moltbot-gateway) +# --keep-firewall Don't delete firewall rule +# --help Show this help message +# +# Examples: +# # Delete everything +# ./scripts/deploy/google/compute-engine/uninstall.sh --project my-project +# +# # Keep firewall rule for future deployments +# ./scripts/deploy/google/compute-engine/uninstall.sh --project my-project --keep-firewall +# +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Default values (matching run.sh defaults) +ZONE="us-central1-a" +INSTANCE_NAME="moltbot-gateway" +PROJECT_ID="" +KEEP_FIREWALL=false + +# Logging functions +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[OK]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } +log_step() { echo -e "\n${CYAN}━━━ $1 ━━━${NC}"; } +log_detail() { echo -e " ${NC}↳ $1"; } + +show_help() { + head -22 "$0" | tail -20 | sed 's/^#//' | sed 's/^ //' + exit 0 +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --project) PROJECT_ID="$2"; shift 2 ;; + --zone) ZONE="$2"; shift 2 ;; + --instance) INSTANCE_NAME="$2"; shift 2 ;; + --keep-firewall) KEEP_FIREWALL=true; shift ;; + --help|-h) show_help ;; + *) log_error "Unknown option: $1"; exit 1 ;; + esac +done + +# Validate required arguments +if [[ -z "$PROJECT_ID" ]]; then + log_error "Project ID is required. Use --project PROJECT_ID" + echo "" + echo "Usage: $0 --project YOUR_PROJECT_ID [OPTIONS]" + exit 1 +fi + +# Check prerequisites +check_prerequisites() { + log_step "Checking prerequisites" + + log_info "Checking if gcloud CLI is installed..." + if ! command -v gcloud &> /dev/null; then + log_error "gcloud CLI is not installed!" + log_detail "Install from: https://cloud.google.com/sdk/docs/install" + exit 1 + fi + log_detail "gcloud CLI found: $(which gcloud)" + + log_info "Checking gcloud authentication..." + if ! gcloud auth print-access-token &> /dev/null; then + log_error "Not authenticated with gcloud!" + log_detail "Run: gcloud auth login" + exit 1 + fi + log_detail "Authenticated as: $(gcloud config get-value account 2>/dev/null)" + + log_success "Prerequisites OK" +} + +# Delete Compute Engine instance +delete_instance() { + log_step "Deleting Compute Engine instance" + + log_info "Checking if instance exists..." + if gcloud compute instances describe "$INSTANCE_NAME" --project="$PROJECT_ID" --zone="$ZONE" &>/dev/null; then + log_detail "Instance found: $INSTANCE_NAME" + log_info "Deleting instance..." + if gcloud compute instances delete "$INSTANCE_NAME" \ + --project="$PROJECT_ID" \ + --zone="$ZONE" \ + --quiet; then + log_success "Instance deleted: $INSTANCE_NAME" + else + log_error "Failed to delete instance" + exit 1 + fi + else + log_warn "Instance not found: $INSTANCE_NAME (already deleted?)" + fi +} + +# Delete firewall rule +delete_firewall() { + if [[ "$KEEP_FIREWALL" == "true" ]]; then + log_step "Keeping firewall rule (--keep-firewall flag)" + log_info "Firewall rule will be preserved for future deployments" + return + fi + + log_step "Deleting firewall rule" + + log_info "Checking firewall rule..." + if gcloud compute firewall-rules describe moltbot-gateway --project="$PROJECT_ID" &>/dev/null; then + log_detail "Firewall rule found, deleting..." + if gcloud compute firewall-rules delete moltbot-gateway \ + --project="$PROJECT_ID" \ + --quiet 2>/dev/null; then + log_success "Firewall rule deleted" + else + log_warn "Failed to delete firewall rule" + fi + else + log_detail "Firewall rule not found (skipping)" + fi +} + +# Main +main() { + echo "" + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ Moltbot Compute Engine Uninstaller ║" + echo "╚════════════════════════════════════════════════════════════╝" + echo "" + echo "Configuration:" + echo " Project: $PROJECT_ID" + echo " Zone: $ZONE" + echo " Instance: $INSTANCE_NAME" + echo " Keep firewall: $KEEP_FIREWALL" + echo "" + + check_prerequisites + delete_instance + delete_firewall + + echo "" + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ Uninstall Complete ║" + echo "╚════════════════════════════════════════════════════════════╝" + echo "" + log_success "Moltbot has been removed from Compute Engine" + echo "" + + if [[ "$KEEP_FIREWALL" == "true" ]]; then + echo "Note: Firewall rule was preserved. To delete it later:" + echo " gcloud compute firewall-rules delete moltbot-gateway --project=$PROJECT_ID" + echo "" + fi + + echo "To redeploy:" + echo " ./scripts/deploy/google/compute-engine/run.sh --project $PROJECT_ID --anthropic-key YOUR_KEY" + echo "" +} + +main