Use Cases

Claude Code × GCP Cloud Run Complete Guide | Serverless Container Auto-Deployment

Speed up GCP Cloud Run deployments with Claude Code. Complete guide with real code examples: Dockerfile generation, auto-scaling, CI/CD pipelines, and Secret Manager integration.

“I want to run containers on GCP, but there are too many settings — I don’t know where to start” — I used to feel exactly the same way. But when I actually got my hands on Cloud Run, I was honestly surprised at how much simpler the configuration was compared to ECS. No VPC setup, no task role design, no cluster management. Just prepare a container image and you get an HTTPS endpoint almost immediately.

Combine that with Claude Code, and everything from Dockerfile generation to Cloud Build CI/CD pipeline setup can be done in a surprisingly short time. This article walks through the steps I actually tried, in 6 clear stages.


Why Cloud Run Is Simpler Than ECS

Cloud Run is GCP’s fully managed container execution service. It uses a “request-driven serverless” model where containers only start when HTTP requests arrive.

Here’s a comparison with ECS:

Cloud Run:
- Cluster management: Not required
- Network setup:      External HTTPS published by default
- Scaling:            Fully automatic from 0 to N
- Billing:            Based on request processing time (nearly free when idle)

AWS ECS/Fargate:
- Cluster management: Required (3 layers: Cluster / Service / TaskDef)
- Network setup:      VPC and security group design required
- Scaling:            Billed for minimum instance count
- Billing:            Based on running task time

For APIs, webhook receivers, batch jobs, and internal microservices — anything that “gets called over HTTP” — Cloud Run is almost always the best fit.


Step 1: Auto-Generate a Dockerfile with Claude Code

Let’s start by generating a production-ready Dockerfile for a TypeScript Node.js API server. Multi-stage builds and non-root user configuration are security requirements.

claude -p "
Generate a production-ready Dockerfile with the following requirements:

- App: TypeScript + Express API (port 8080)
- Node.js version: 22 LTS
- Multi-stage build (builder → runner)
- Run as non-root user (node user)
- Also generate .dockerignore
- Support the PORT environment variable as recommended by Cloud Run
- Health check: /health endpoint
"

Generated Dockerfile:

# ---- builder stage ----
FROM node:22-alpine AS builder
WORKDIR /app

# Copy dependencies first to leverage build cache
COPY package*.json ./
RUN npm ci --only=production=false

COPY tsconfig.json ./
COPY src ./src
RUN npm run build

# ---- runner stage ----
FROM node:22-alpine AS runner
WORKDIR /app

# Security: run as non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Install production dependencies only
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Copy build artifacts
COPY --from=builder /app/dist ./dist

# Cloud Run passes the port via the PORT environment variable
ENV PORT=8080
ENV NODE_ENV=production

# Switch to non-root user
USER appuser

EXPOSE 8080
CMD ["node", "dist/index.js"]

Generated .dockerignore:

node_modules
dist
.env
.env.*
*.log
.git
.gitignore
README.md

Step 2: First Deployment to Cloud Run

Once the Dockerfile is ready, generate the commands to deploy to Cloud Run.

claude -p "
Generate the commands to deploy to GCP Cloud Run with the following requirements:

- Project ID: my-project-123
- Region: asia-northeast1 (Tokyo)
- Service name: myapp-api
- Memory: 512Mi
- CPU: 1
- Concurrency: 80
- Authentication: allow unauthenticated requests
- Environment variables: NODE_ENV=production
- Port: 8080
"

Generated commands:

# 1. Build and push the Docker image to Artifact Registry
gcloud auth configure-docker asia-northeast1-docker.pkg.dev

docker build -t asia-northeast1-docker.pkg.dev/my-project-123/myapp/api:v1.0.0 .
docker push asia-northeast1-docker.pkg.dev/my-project-123/myapp/api:v1.0.0

# 2. Deploy to Cloud Run
gcloud run deploy myapp-api \
  --image asia-northeast1-docker.pkg.dev/my-project-123/myapp/api:v1.0.0 \
  --region asia-northeast1 \
  --platform managed \
  --memory 512Mi \
  --cpu 1 \
  --concurrency 80 \
  --allow-unauthenticated \
  --set-env-vars NODE_ENV=production \
  --port 8080

After a successful deployment, you’ll get a URL like https://myapp-api-xxxx-an.a.run.app.


Step 3: Configure Auto-Scaling Properly

Using Cloud Run’s default settings can lead to instance explosions during traffic spikes or frequent cold starts. The following settings are critical for production.

claude -p "
Optimize the Cloud Run scaling configuration.

Requirements:
- Almost no traffic at night (cost savings prioritized)
- Normal: 1–3 instances
- Peak: scale out to a maximum of 20 instances
- Scale out when CPU usage exceeds 70%
- Minimize cold starts
- Max concurrent connections per instance: 80
"

Generated command:

gcloud run services update myapp-api \
  --region asia-northeast1 \
  --min-instances 1 \
  --max-instances 20 \
  --concurrency 80 \
  --cpu-throttling \
  --execution-environment gen2

CPU-based scaling configuration file (service.yaml):

# service.yaml - Cloud Run service configuration
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: myapp-api
  annotations:
    run.googleapis.com/launch-stage: GA
spec:
  template:
    metadata:
      annotations:
        # Minimum instance count (cold start mitigation)
        autoscaling.knative.dev/minScale: "1"
        # Maximum instance count (cost ceiling)
        autoscaling.knative.dev/maxScale: "20"
        # Scale out at 70% CPU usage
        autoscaling.knative.dev/target-utilization-percentage: "70"
        # Gen2 execution environment (better performance)
        run.googleapis.com/execution-environment: gen2
    spec:
      containerConcurrency: 80
      containers:
        - image: asia-northeast1-docker.pkg.dev/my-project-123/myapp/api:latest
          resources:
            limits:
              memory: 512Mi
              cpu: "1"

Step 4: Manage Environment Variables Safely with Secret Manager

Hardcoding database passwords or API keys into environment variables is absolutely forbidden. Integrate with GCP Secret Manager to manage sensitive information securely.

claude -p "
Generate the code and configuration to integrate GCP Secret Manager with Cloud Run.

- Secrets: DATABASE_URL, SENDGRID_API_KEY, JWT_SECRET
- Grant read permission to the Cloud Run service account
- TypeScript code to read secrets in the application
- Include gcloud commands
"

Generated configuration:

# Register secrets
echo -n "postgresql://user:password@host:5432/db" | \
  gcloud secrets create DATABASE_URL --data-file=-

echo -n "SG.xxxxxxxxxxxx" | \
  gcloud secrets create SENDGRID_API_KEY --data-file=-

echo -n "your-jwt-secret-here" | \
  gcloud secrets create JWT_SECRET --data-file=-

# Grant read permission to the service account
gcloud projects add-iam-policy-binding my-project-123 \
  --member="serviceAccount:[email protected]" \
  --role="roles/secretmanager.secretAccessor"

# Mount secrets in Cloud Run
gcloud run services update myapp-api \
  --region asia-northeast1 \
  --set-secrets="DATABASE_URL=DATABASE_URL:latest,SENDGRID_API_KEY=SENDGRID_API_KEY:latest,JWT_SECRET=JWT_SECRET:latest"

TypeScript config loader (src/config.ts):

// src/config.ts
// In Cloud Run, secrets are injected as environment variables automatically,
// so we can read them with process.env directly
export const config = {
  databaseUrl: process.env.DATABASE_URL!,
  sendgridApiKey: process.env.SENDGRID_API_KEY!,
  jwtSecret: process.env.JWT_SECRET!,
  port: parseInt(process.env.PORT || "8080", 10),
  nodeEnv: process.env.NODE_ENV || "development",
};

// Validate required secrets on startup
const requiredEnvVars = ["DATABASE_URL", "SENDGRID_API_KEY", "JWT_SECRET"];
for (const envVar of requiredEnvVars) {
  if (!process.env[envVar]) {
    console.error(`Missing required environment variable: ${envVar}`);
    process.exit(1);
  }
}

Step 5: Build a CI/CD Pipeline with Cloud Build

Manually building and deploying every time is inefficient. Integrate Cloud Build with GitHub so that auto-deployment triggers on every push to the main branch.

claude -p "
Set up a CI/CD pipeline: GitHub → Cloud Build → Cloud Run.

Requirements:
- Trigger on push to main branch
- Run tests → build Docker image → push to Artifact Registry → deploy to Cloud Run
- Slack notification on deployment failure
- Generate cloudbuild.yaml
"

Generated cloudbuild.yaml:

# cloudbuild.yaml
steps:
  # Step 1: Install dependencies and run tests
  - name: "node:22-alpine"
    id: "test"
    entrypoint: "sh"
    args:
      - "-c"
      - |
        npm ci
        npm run test
        npm run lint

  # Step 2: Build Docker image
  - name: "gcr.io/cloud-builders/docker"
    id: "build"
    args:
      - "build"
      - "-t"
      - "asia-northeast1-docker.pkg.dev/$PROJECT_ID/myapp/api:$COMMIT_SHA"
      - "-t"
      - "asia-northeast1-docker.pkg.dev/$PROJECT_ID/myapp/api:latest"
      - "."

  # Step 3: Push to Artifact Registry
  - name: "gcr.io/cloud-builders/docker"
    id: "push"
    args:
      - "push"
      - "--all-tags"
      - "asia-northeast1-docker.pkg.dev/$PROJECT_ID/myapp/api"

  # Step 4: Deploy to Cloud Run
  - name: "gcr.io/google.com/cloudsdktool/cloud-sdk"
    id: "deploy"
    entrypoint: "gcloud"
    args:
      - "run"
      - "deploy"
      - "myapp-api"
      - "--image"
      - "asia-northeast1-docker.pkg.dev/$PROJECT_ID/myapp/api:$COMMIT_SHA"
      - "--region"
      - "asia-northeast1"
      - "--platform"
      - "managed"
      - "--quiet"

  # Step 5: Slack notification (on success)
  - name: "curlimages/curl"
    id: "notify-success"
    entrypoint: "curl"
    args:
      - "-X"
      - "POST"
      - "-H"
      - "Content-type: application/json"
      - "--data"
      - '{"text":"✅ Cloud Run deployment complete: $COMMIT_SHA"}'
      - "$_SLACK_WEBHOOK_URL"

images:
  - "asia-northeast1-docker.pkg.dev/$PROJECT_ID/myapp/api:$COMMIT_SHA"
  - "asia-northeast1-docker.pkg.dev/$PROJECT_ID/myapp/api:latest"

options:
  logging: CLOUD_LOGGING_ONLY
  machineType: E2_HIGHCPU_8

timeout: "1200s"

Step 6: Custom Domain and Load Balancer

To use a custom domain instead of the default Cloud Run URL, routing through a GCP load balancer is recommended.

claude -p "
Generate the steps and Terraform code to configure a custom domain api.example.com for Cloud Run.

- Use Google-managed SSL certificate
- DDoS protection with Cloud Armor
- Route through load balancer to Cloud Run
"

Generated Terraform code (main.tf):

# main.tf - Cloud Run + Load Balancer + SSL

# Cloud Run service backend
resource "google_compute_region_network_endpoint_group" "cloudrun_neg" {
  name                  = "myapp-neg"
  network_endpoint_type = "SERVERLESS"
  region                = "asia-northeast1"

  cloud_run {
    service = "myapp-api"
  }
}

# Backend service
resource "google_compute_backend_service" "default" {
  name     = "myapp-backend"
  protocol = "HTTP"

  backend {
    group = google_compute_region_network_endpoint_group.cloudrun_neg.id
  }

  # Cloud Armor security policy
  security_policy = google_compute_security_policy.policy.id
}

# Google-managed SSL certificate
resource "google_compute_managed_ssl_certificate" "default" {
  name = "myapp-ssl-cert"
  managed {
    domains = ["api.example.com"]
  }
}

# HTTPS Load Balancer
resource "google_compute_url_map" "default" {
  name            = "myapp-url-map"
  default_service = google_compute_backend_service.default.id
}

resource "google_compute_target_https_proxy" "default" {
  name             = "myapp-https-proxy"
  url_map          = google_compute_url_map.default.id
  ssl_certificates = [google_compute_managed_ssl_certificate.default.id]
}

resource "google_compute_global_forwarding_rule" "https" {
  name       = "myapp-https-forwarding-rule"
  target     = google_compute_target_https_proxy.default.id
  port_range = "443"
  ip_address = google_compute_global_address.default.address
}

# Cloud Armor DDoS protection policy
resource "google_compute_security_policy" "policy" {
  name = "myapp-security-policy"

  rule {
    action   = "deny(403)"
    priority = "1000"
    match {
      expr {
        expression = "evaluatePreconfiguredExpr('sqli-stable')"
      }
    }
    description = "SQL injection protection"
  }

  rule {
    action   = "allow"
    priority = "2147483647"
    match {
      versioned_expr = "SRC_IPS_V1"
      config {
        src_ip_ranges = ["*"]
      }
    }
    description = "Default allow"
  }
}

Top 5 Pitfalls

1. Timeouts caused by cold starts

When there’s no traffic for a period of time, Cloud Run scales instances down to zero (if min-instances is 0). The next request must wait for a container to start, which can cause a 1–2 second delay. Set min-instances to at least 1 in production.

# Cold start mitigation: keep at least 1 instance warm
gcloud run services update myapp-api \
  --min-instances 1 \
  --region asia-northeast1

2. Forgetting to handle SIGTERM

Cloud Run sends SIGTERM during scale-in or deployment. Without proper handling, in-flight requests will be forcibly terminated.

// src/index.ts - Handle SIGTERM properly
import express from "express";
const app = express();
const server = app.listen(process.env.PORT || 8080);

// Graceful shutdown
process.on("SIGTERM", () => {
  console.log("SIGTERM received. Graceful shutdown started.");
  server.close(() => {
    console.log("HTTP server closed.");
    process.exit(0);
  });

  // Force exit if not done within 30 seconds
  setTimeout(() => {
    console.error("Forced shutdown after timeout.");
    process.exit(1);
  }, 30000);
});

3. Hardcoding sensitive data in environment variables

# ❌ Never do this: hardcoding secrets
gcloud run services update myapp-api \
  --set-env-vars DATABASE_PASSWORD=mypassword123

# ✅ Correct: pass secrets safely via Secret Manager
gcloud run services update myapp-api \
  --set-secrets="DATABASE_PASSWORD=DATABASE_PASSWORD:latest"

4. Insufficient memory configuration

Node.js has a small default heap size. Running Node.js in a 512Mi container can cause OOM (Out of Memory) crashes when processing large data.

# Memory fix: explicitly set Node.js heap size
CMD ["node", "--max-old-space-size=384", "dist/index.js"]

5. Running background processing outside of requests

Cloud Run CPUs are essentially unavailable when not processing requests (CPU throttling). If you need periodic background processing, combine with Cloud Scheduler or use always-on CPU mode.

# Always-on CPU mode (for services needing background processing)
gcloud run services update myapp-api \
  --no-cpu-throttling \
  --region asia-northeast1

Summary

TaskClaude Code’s Contribution
Dockerfile generationMulti-stage build and non-root user setup automated
First deploymentComplete gcloud commands generated from requirements
Scaling configurationmin/max instances and CPU thresholds optimized
Secret Manager integrationSecret creation, permissions, and mount config generated
CI/CD pipelinecloudbuild.yaml with tests generated
Custom domainLoad balancer Terraform config auto-generated

Cloud Run is dramatically simpler than ECS when it comes to just getting something running. With Claude Code, you can go from Dockerfile to a full CI/CD pipeline in under an hour. Try Steps 1–3 first and experience how comfortable Cloud Run can be.

References

#claude-code #gcp #cloud-run #docker #typescript #serverless
Free

Free PDF: Claude Code Cheatsheet in 5 Minutes

Just enter your email and we'll send you the single-page A4 cheatsheet right away.

We handle your data with care and never send spam.

Level up your Claude Code workflow

50 battle-tested prompt templates you can copy-paste into Claude Code right now.

Masa

About the Author

Masa

Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.