openclaw/scripts/deploy/google/compute-engine/run.sh
velrino ad9324ddbb 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
2026-01-29 19:14:59 -03:00

980 lines
35 KiB
Bash
Executable File

#!/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