diff --git a/Dockerfile.production b/Dockerfile.production new file mode 100644 index 000000000..67e9e916b --- /dev/null +++ b/Dockerfile.production @@ -0,0 +1,80 @@ +# ============================================ +# Dockerfile.production +# Minimal multi-stage build for GCP deployment +# Final image: ~150-200MB +# ============================================ + +# -------------------------------------------- +# Stage 1: Builder +# -------------------------------------------- +FROM node:22-bookworm AS builder + +# Install Bun (required for build scripts) +RUN curl -fsSL https://bun.sh/install | bash +ENV PATH="/root/.bun/bin:${PATH}" + +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /app + +# Copy package files for dependency caching +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ +COPY ui/package.json ./ui/package.json +COPY patches ./patches +COPY scripts ./scripts + +# Install ALL dependencies (including dev for build) +RUN pnpm install --frozen-lockfile + +# Copy source and build +COPY . . +RUN pnpm build + +# Build UI +ENV CLAWDBOT_PREFER_PNPM=1 +RUN pnpm ui:install +RUN pnpm ui:build + +# Prune dev dependencies for production +RUN pnpm prune --prod + +# -------------------------------------------- +# Stage 2: Production (Minimal) +# -------------------------------------------- +FROM node:22-bookworm-slim AS production + +# Only essential runtime packages +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +WORKDIR /app + +# Copy only production artifacts +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./ +COPY --from=builder /app/assets ./assets +COPY --from=builder /app/skills ./skills +COPY --from=builder /app/extensions ./extensions +COPY --from=builder /app/docs ./docs + +# Copy UI build output if present +COPY --from=builder /app/ui/dist ./ui/dist + +# Create non-root user (node user already exists in node image) +RUN chown -R node:node /app +USER node + +# Gateway port +EXPOSE 18789 + +# Health check +HEALTHCHECK --interval=60s --timeout=5s --retries=2 \ + CMD curl -sf http://localhost:18789/health || exit 1 + +ENV NODE_ENV=production + +CMD ["node", "dist/index.js", "gateway", "--bind", "lan", "--port", "18789"] diff --git a/docs/platforms/gcp.md b/docs/platforms/gcp.md index cffa03ace..b77a5849d 100644 --- a/docs/platforms/gcp.md +++ b/docs/platforms/gcp.md @@ -15,6 +15,51 @@ Run a persistent Clawdbot Gateway on a GCP Compute Engine VM using Docker, with If you want "Clawdbot 24/7 for ~$5-12/mo", this is a reliable setup on Google Cloud. Pricing varies by machine type and region; pick the smallest VM that fits your workload and scale up if you hit OOMs. +--- + +## Minimal container deployment (scripted) + +For a streamlined deployment with a smaller image (~150MB), use the provided scripts: + +```bash +cd scripts/gcp + +# 1. Create a NEW GCP project (use a unique name) +./setup-project.sh clawdbot-$(date +%Y%m%d) us-east1 + +# 2. Store secrets (interactive prompts) +./store-secrets.sh + +# 3. Build production image +./build-push.sh local # or: ./build-push.sh cloud + +# 4. Deploy +./deploy-vm.sh # e2-micro (free tier) +./deploy-vm.sh e2-small # 2GB RAM + +# 5. Connect via IAP tunnel +./connect.sh +``` + +This uses `Dockerfile.production` (multi-stage build) and deploys with: +- No public IP (secure IAP tunnel access) +- Secrets in GCP Secret Manager +- Container-optimized VM with auto-restart + +See `scripts/gcp/README.md` for full details. + +**Cost comparison:** + +| Option | Monthly Cost | Notes | +|--------|--------------|-------| +| e2-micro VM | $0 (free tier) | 1 vCPU shared, 1GB RAM | +| e2-small VM | ~$13/mo | 2 vCPU shared, 2GB RAM | +| Cloud Run | ~$0-5/mo | Pay per request, scales to 0 | + +--- + +The rest of this guide covers manual setup with full control over the Docker configuration. + ## What are we doing (simple terms)? - Create a GCP project and enable billing diff --git a/scripts/gcp/README.md b/scripts/gcp/README.md new file mode 100644 index 000000000..bee5eb454 --- /dev/null +++ b/scripts/gcp/README.md @@ -0,0 +1,61 @@ +# GCP Deployment Scripts + +Minimal container deployment for Clawdbot on Google Cloud Platform. + +## Quick Start + +```bash +# 1. Create a NEW GCP project and enable APIs +./setup-project.sh clawdbot-$(date +%Y%m%d) us-east1 + +# 2. Store secrets (interactive) +./store-secrets.sh + +# 3. Build and push production image +./build-push.sh local # Build locally, push to GCR +./build-push.sh cloud # Build on Cloud Build (no local Docker) + +# 4. Deploy +./deploy-vm.sh # Compute Engine (free tier: e2-micro) +./deploy-vm.sh e2-small # Compute Engine with more RAM +./deploy-cloudrun.sh # Cloud Run (pay per request) + +# 5. Connect +./connect.sh # IAP tunnel to Gateway +./ssh.sh # SSH into VM +./logs.sh --follow # View container logs +``` + +## Cost Comparison + +| Option | Monthly Cost | Notes | +|--------|--------------|-------| +| e2-micro VM | $0 (free tier) | 1 vCPU shared, 1GB RAM, always on | +| e2-small VM | ~$13/mo | 2 vCPU shared, 2GB RAM | +| Cloud Run | ~$0-5/mo | Pay per request, auto-scales to 0 | + +## Files + +- `setup-project.sh` - Enable APIs, create service account, firewall rules +- `store-secrets.sh` - Store API keys in Secret Manager +- `build-push.sh` - Build production image and push to GCR +- `deploy-vm.sh` - Deploy to Compute Engine with container +- `deploy-cloudrun.sh` - Deploy to Cloud Run +- `connect.sh` - IAP tunnel to Gateway port +- `ssh.sh` - SSH into VM via IAP +- `logs.sh` - View container logs + +## Production Image + +Uses `Dockerfile.production` (multi-stage build): +- Builder stage: Full Node.js + Bun for compilation +- Production stage: `node:22-bookworm-slim` (~150-200MB final) +- Runs as non-root user +- Health check endpoint + +## Security + +- No public IP (access via IAP tunnel) +- Secrets stored in Secret Manager +- Dedicated service account with minimal permissions +- Shielded VM with Secure Boot diff --git a/scripts/gcp/build-push.sh b/scripts/gcp/build-push.sh new file mode 100755 index 000000000..09aa337e9 --- /dev/null +++ b/scripts/gcp/build-push.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Build and push production Docker image to GCR +# Usage: ./build-push.sh [local|cloud] +set -euo pipefail + +BUILD_MODE="${1:-local}" +PROJECT_ID=$(gcloud config get-value project 2>/dev/null) + +if [[ -z "$PROJECT_ID" ]]; then + echo "Error: No GCP project configured. Run: gcloud config set project PROJECT_ID" + exit 1 +fi + +IMAGE="gcr.io/${PROJECT_ID}/clawdbot:latest" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +echo "Building image: $IMAGE" +echo "Build mode: $BUILD_MODE" +echo "" + +cd "$REPO_ROOT" + +if [[ "$BUILD_MODE" == "cloud" ]]; then + # Build on GCP Cloud Build (no local Docker needed) + echo "Building on Cloud Build..." + gcloud builds submit \ + --tag "$IMAGE" \ + --timeout=1800s \ + -f Dockerfile.production \ + . +else + # Build locally and push + echo "Building locally..." + docker build \ + -f Dockerfile.production \ + -t "$IMAGE" \ + . + + echo "" + echo "Pushing to GCR..." + docker push "$IMAGE" +fi + +echo "" +echo "Image pushed: $IMAGE" +echo "" +echo "Next: Deploy with ./deploy-vm.sh or ./deploy-cloudrun.sh" diff --git a/scripts/gcp/connect.sh b/scripts/gcp/connect.sh new file mode 100755 index 000000000..1fa7ba64d --- /dev/null +++ b/scripts/gcp/connect.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Connect to Clawdbot Gateway via IAP tunnel +# Usage: ./connect.sh [VM_NAME] +set -euo pipefail + +VM_NAME="${1:-clawdbot-vm}" +PROJECT_ID=$(gcloud config get-value project 2>/dev/null) +REGION=$(gcloud config get-value compute/region 2>/dev/null) +ZONE="${REGION}-b" + +if [[ -z "$PROJECT_ID" ]]; then + echo "Error: No GCP project configured. Run: gcloud config set project PROJECT_ID" + exit 1 +fi + +echo "Starting IAP tunnel to Clawdbot Gateway" +echo " VM: $VM_NAME" +echo " Zone: $ZONE" +echo "" +echo "Access at: http://localhost:18789" +echo "Press Ctrl+C to disconnect" +echo "" + +gcloud compute start-iap-tunnel "$VM_NAME" 18789 \ + --local-host-port=localhost:18789 \ + --zone="$ZONE" \ + --project="$PROJECT_ID" diff --git a/scripts/gcp/deploy-cloudrun.sh b/scripts/gcp/deploy-cloudrun.sh new file mode 100755 index 000000000..87af44c33 --- /dev/null +++ b/scripts/gcp/deploy-cloudrun.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Deploy Clawdbot to Cloud Run (pay-per-request, scales to zero) +# Usage: ./deploy-cloudrun.sh +set -euo pipefail + +PROJECT_ID=$(gcloud config get-value project 2>/dev/null) +REGION=$(gcloud config get-value compute/region 2>/dev/null) + +if [[ -z "$PROJECT_ID" ]]; then + echo "Error: No GCP project configured. Run: gcloud config set project PROJECT_ID" + exit 1 +fi + +IMAGE="gcr.io/${PROJECT_ID}/clawdbot:latest" +SA_EMAIL="clawdbot-sa@${PROJECT_ID}.iam.gserviceaccount.com" +SERVICE_NAME="clawdbot" + +echo "Deploying Clawdbot to Cloud Run" +echo " Project: $PROJECT_ID" +echo " Region: $REGION" +echo " Image: $IMAGE" +echo "" + +# Build secret references +SECRETS="" +if gcloud secrets describe anthropic-api-key &>/dev/null; then + SECRETS="${SECRETS}ANTHROPIC_API_KEY=anthropic-api-key:latest," +fi +if gcloud secrets describe openai-api-key &>/dev/null; then + SECRETS="${SECRETS}OPENAI_API_KEY=openai-api-key:latest," +fi +if gcloud secrets describe clawdbot-gateway-token &>/dev/null; then + SECRETS="${SECRETS}CLAWDBOT_GATEWAY_TOKEN=clawdbot-gateway-token:latest," +fi + +# Remove trailing comma +SECRETS="${SECRETS%,}" + +# Deploy +gcloud run deploy "$SERVICE_NAME" \ + --image="$IMAGE" \ + --platform=managed \ + --region="$REGION" \ + --memory=512Mi \ + --cpu=1 \ + --min-instances=0 \ + --max-instances=1 \ + --port=18789 \ + --no-allow-unauthenticated \ + --service-account="$SA_EMAIL" \ + ${SECRETS:+--set-secrets="$SECRETS"} + +echo "" +echo "Deployment complete!" +echo "" + +# Get service URL +SERVICE_URL=$(gcloud run services describe "$SERVICE_NAME" --region="$REGION" --format='value(status.url)') +echo "Service URL: $SERVICE_URL" +echo "" +echo "Note: Cloud Run requires authentication. Use:" +echo " gcloud run services add-iam-policy-binding $SERVICE_NAME \\" +echo " --region=$REGION \\" +echo " --member='user:YOUR_EMAIL' \\" +echo " --role='roles/run.invoker'" +echo "" +echo "Or for public access (not recommended):" +echo " gcloud run services add-iam-policy-binding $SERVICE_NAME \\" +echo " --region=$REGION \\" +echo " --member='allUsers' \\" +echo " --role='roles/run.invoker'" diff --git a/scripts/gcp/deploy-vm.sh b/scripts/gcp/deploy-vm.sh new file mode 100755 index 000000000..6949e6efb --- /dev/null +++ b/scripts/gcp/deploy-vm.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Deploy Clawdbot to GCP Compute Engine VM with Container-Optimized OS +# Usage: ./deploy-vm.sh [MACHINE_TYPE] +set -euo pipefail + +MACHINE_TYPE="${1:-e2-micro}" +PROJECT_ID=$(gcloud config get-value project 2>/dev/null) +REGION=$(gcloud config get-value compute/region 2>/dev/null) +ZONE="${REGION}-b" + +if [[ -z "$PROJECT_ID" ]]; then + echo "Error: No GCP project configured. Run: gcloud config set project PROJECT_ID" + exit 1 +fi + +IMAGE="gcr.io/${PROJECT_ID}/clawdbot:latest" +SA_EMAIL="clawdbot-sa@${PROJECT_ID}.iam.gserviceaccount.com" +VM_NAME="clawdbot-vm" + +echo "Deploying Clawdbot Gateway" +echo " Project: $PROJECT_ID" +echo " Region: $REGION" +echo " Zone: $ZONE" +echo " Machine type: $MACHINE_TYPE" +echo " Image: $IMAGE" +echo "" + +# Check if VM exists +if gcloud compute instances describe "$VM_NAME" --zone="$ZONE" &>/dev/null; then + echo "VM already exists. Updating container..." + gcloud compute instances update-container "$VM_NAME" \ + --zone="$ZONE" \ + --container-image="$IMAGE" + echo "" + echo "Container updated. Restarting VM..." + gcloud compute instances reset "$VM_NAME" --zone="$ZONE" +else + echo "Creating new VM..." + gcloud compute instances create-with-container "$VM_NAME" \ + --project="$PROJECT_ID" \ + --zone="$ZONE" \ + --machine-type="$MACHINE_TYPE" \ + --network-interface=network-tier=STANDARD,subnet=default,no-address \ + --shielded-secure-boot \ + --shielded-vtpm \ + --shielded-integrity-monitoring \ + --container-image="$IMAGE" \ + --container-restart-policy=always \ + --container-env=NODE_ENV=production,GATEWAY_PORT=18789,GATEWAY_BIND=0.0.0.0 \ + --metadata=enable-oslogin=TRUE \ + --tags=clawdbot \ + --boot-disk-size=10GB \ + --boot-disk-type=pd-standard \ + --service-account="$SA_EMAIL" \ + --scopes=https://www.googleapis.com/auth/cloud-platform +fi + +echo "" +echo "Deployment complete!" +echo "" +echo "Machine type costs (approximate):" +echo " e2-micro: Free tier eligible (1 vCPU shared, 1GB RAM)" +echo " e2-small: ~\$13/mo (2 vCPU shared, 2GB RAM)" +echo " e2-medium: ~\$25/mo (2 vCPU, 4GB RAM)" +echo "" +echo "Connect with: ./connect.sh" +echo "Or manually: gcloud compute start-iap-tunnel $VM_NAME 18789 --local-host-port=localhost:18789 --zone=$ZONE" diff --git a/scripts/gcp/logs.sh b/scripts/gcp/logs.sh new file mode 100755 index 000000000..9bae20796 --- /dev/null +++ b/scripts/gcp/logs.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# View Clawdbot container logs on GCP VM +# Usage: ./logs.sh [VM_NAME] [--follow] +set -euo pipefail + +VM_NAME="${1:-clawdbot-vm}" +FOLLOW="${2:-}" +PROJECT_ID=$(gcloud config get-value project 2>/dev/null) +REGION=$(gcloud config get-value compute/region 2>/dev/null) +ZONE="${REGION}-b" + +if [[ -z "$PROJECT_ID" ]]; then + echo "Error: No GCP project configured. Run: gcloud config set project PROJECT_ID" + exit 1 +fi + +echo "Fetching logs from $VM_NAME..." + +if [[ "$FOLLOW" == "--follow" ]] || [[ "$FOLLOW" == "-f" ]]; then + gcloud compute ssh "$VM_NAME" \ + --zone="$ZONE" \ + --tunnel-through-iap \ + --command="sudo journalctl -u konlet-startup -f" +else + gcloud compute ssh "$VM_NAME" \ + --zone="$ZONE" \ + --tunnel-through-iap \ + --command="sudo journalctl -u konlet-startup -n 100" +fi diff --git a/scripts/gcp/setup-project.sh b/scripts/gcp/setup-project.sh new file mode 100755 index 000000000..06730c546 --- /dev/null +++ b/scripts/gcp/setup-project.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Create and setup a NEW GCP project for Clawdbot deployment +# Usage: ./setup-project.sh PROJECT_ID [REGION] +set -euo pipefail + +PROJECT_ID="${1:-}" +REGION="${2:-us-east1}" + +if [[ -z "$PROJECT_ID" ]]; then + echo "Usage: $0 PROJECT_ID [REGION]" + echo "Example: $0 clawdbot-$(date +%Y%m%d) us-east1" + exit 1 +fi + +echo "Creating NEW GCP project: $PROJECT_ID in region: $REGION" +echo "" + +# Create the project +if gcloud projects describe "$PROJECT_ID" &>/dev/null; then + echo "Project $PROJECT_ID already exists, using it..." +else + echo "Creating project $PROJECT_ID..." + gcloud projects create "$PROJECT_ID" --name="Clawdbot Gateway" +fi + +# Set as active project +gcloud config set project "$PROJECT_ID" +gcloud config set compute/region "$REGION" + +# Check billing +echo "" +echo "Checking billing..." +BILLING=$(gcloud billing projects describe "$PROJECT_ID" --format='value(billingEnabled)' 2>/dev/null || echo "false") +if [[ "$BILLING" != "True" ]]; then + echo "" + echo "WARNING: Billing is not enabled for this project." + echo "Enable billing at: https://console.cloud.google.com/billing/linkedaccount?project=$PROJECT_ID" + echo "" + read -rp "Press Enter after enabling billing to continue..." +fi + +# Enable required APIs +echo "Enabling required APIs..." +gcloud services enable \ + compute.googleapis.com \ + secretmanager.googleapis.com \ + containerregistry.googleapis.com \ + cloudbuild.googleapis.com \ + iap.googleapis.com + +# Create service account +SA_NAME="clawdbot-sa" +SA_EMAIL="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" + +echo "Creating service account: $SA_NAME" +if ! gcloud iam service-accounts describe "$SA_EMAIL" &>/dev/null; then + gcloud iam service-accounts create "$SA_NAME" \ + --display-name="Clawdbot Service Account" +else + echo "Service account already exists" +fi + +# Create firewall rule for IAP +echo "Creating firewall rule for IAP..." +if ! gcloud compute firewall-rules describe allow-iap-clawdbot &>/dev/null; then + gcloud compute firewall-rules create allow-iap-clawdbot \ + --direction=INGRESS \ + --priority=1000 \ + --network=default \ + --action=ALLOW \ + --rules=tcp:18789,tcp:22 \ + --source-ranges=35.235.240.0/20 \ + --target-tags=clawdbot +else + echo "Firewall rule already exists" +fi + +echo "" +echo "Project setup complete!" +echo "" +echo "Next steps:" +echo " 1. Store secrets: ./store-secrets.sh" +echo " 2. Build and push image: ./build-push.sh" +echo " 3. Deploy VM: ./deploy-vm.sh" diff --git a/scripts/gcp/ssh.sh b/scripts/gcp/ssh.sh new file mode 100755 index 000000000..af4887845 --- /dev/null +++ b/scripts/gcp/ssh.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# SSH into Clawdbot VM via IAP +# Usage: ./ssh.sh [VM_NAME] +set -euo pipefail + +VM_NAME="${1:-clawdbot-vm}" +PROJECT_ID=$(gcloud config get-value project 2>/dev/null) +REGION=$(gcloud config get-value compute/region 2>/dev/null) +ZONE="${REGION}-b" + +if [[ -z "$PROJECT_ID" ]]; then + echo "Error: No GCP project configured. Run: gcloud config set project PROJECT_ID" + exit 1 +fi + +echo "Connecting to $VM_NAME via IAP tunnel..." +gcloud compute ssh "$VM_NAME" \ + --zone="$ZONE" \ + --tunnel-through-iap diff --git a/scripts/gcp/store-secrets.sh b/scripts/gcp/store-secrets.sh new file mode 100755 index 000000000..85277b511 --- /dev/null +++ b/scripts/gcp/store-secrets.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Store secrets in GCP Secret Manager +# Usage: ./store-secrets.sh +set -euo pipefail + +PROJECT_ID=$(gcloud config get-value project 2>/dev/null) + +if [[ -z "$PROJECT_ID" ]]; then + echo "Error: No GCP project configured. Run: gcloud config set project PROJECT_ID" + exit 1 +fi + +echo "Storing secrets for project: $PROJECT_ID" +echo "" + +# Function to create or update a secret +create_secret() { + local name="$1" + local prompt="$2" + + echo -n "$prompt: " + read -rs value + echo "" + + if [[ -z "$value" ]]; then + echo "Skipping $name (empty value)" + return + fi + + if gcloud secrets describe "$name" &>/dev/null; then + echo "$value" | gcloud secrets versions add "$name" --data-file=- + echo "Updated secret: $name" + else + echo "$value" | gcloud secrets create "$name" --data-file=- + echo "Created secret: $name" + fi +} + +# Gateway token +echo "Enter your Gateway token (generate with: openssl rand -hex 32)" +create_secret "clawdbot-gateway-token" "Gateway token" + +# Anthropic API key (optional) +echo "" +echo "Enter your Anthropic API key (optional, press Enter to skip)" +create_secret "anthropic-api-key" "Anthropic API key" + +# OpenAI API key (optional) +echo "" +echo "Enter your OpenAI API key (optional, press Enter to skip)" +create_secret "openai-api-key" "OpenAI API key" + +# Grant service account access to secrets +SA_EMAIL="clawdbot-sa@${PROJECT_ID}.iam.gserviceaccount.com" + +echo "" +echo "Granting secret access to service account..." + +for secret in clawdbot-gateway-token anthropic-api-key openai-api-key; do + if gcloud secrets describe "$secret" &>/dev/null; then + gcloud secrets add-iam-policy-binding "$secret" \ + --member="serviceAccount:$SA_EMAIL" \ + --role="roles/secretmanager.secretAccessor" \ + --quiet 2>/dev/null || true + fi +done + +echo "" +echo "Secrets configured!" +echo "" +echo "Verify with: gcloud secrets list"