GCP: add minimal container deployment with scripts

Co-Authored-By: Warp <agent@warp.dev>
This commit is contained in:
Jonathan Nelson 2026-01-26 16:54:05 -05:00
parent ff382f6b68
commit c063ce3e3a
No known key found for this signature in database
11 changed files with 602 additions and 0 deletions

80
Dockerfile.production Normal file
View File

@ -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"]

View File

@ -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

61
scripts/gcp/README.md Normal file
View File

@ -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

48
scripts/gcp/build-push.sh Executable file
View File

@ -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"

27
scripts/gcp/connect.sh Executable file
View File

@ -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"

71
scripts/gcp/deploy-cloudrun.sh Executable file
View File

@ -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'"

67
scripts/gcp/deploy-vm.sh Executable file
View File

@ -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"

29
scripts/gcp/logs.sh Executable file
View File

@ -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

84
scripts/gcp/setup-project.sh Executable file
View File

@ -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"

19
scripts/gcp/ssh.sh Executable file
View File

@ -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

71
scripts/gcp/store-secrets.sh Executable file
View File

@ -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"