Deploying to Google Cloud
This guide covers the full deployment workflow for CruzJS on Google Cloud, including GitHub Actions CI/CD, Terraform for infrastructure, and database migration strategies.
Prerequisites
Section titled “Prerequisites”gcloudCLI authenticated (gcloud auth login)- A GCP project with billing enabled
- Artifact Registry repository for Docker images
- Cloud SQL instance (see GCP Adapter)
Artifact Registry Setup
Section titled “Artifact Registry Setup”# Enable the APIgcloud services enable artifactregistry.googleapis.com
# Create a Docker repositorygcloud artifacts repositories create cruz-repo \ --repository-format=docker \ --location=us-central1 \ --description="CruzJS app images"
# Configure Docker authgcloud auth configure-docker us-central1-docker.pkg.devGitHub Actions Pipeline
Section titled “GitHub Actions Pipeline”This workflow builds, pushes, and deploys on every push to main.
name: Deploy to GCPon: push: branches: [main]
env: PROJECT_ID: my-gcp-project REGION: us-central1 SERVICE: my-cruz-app REPO: cruz-repo IMAGE: us-central1-docker.pkg.dev/my-gcp-project/cruz-repo/cruz-app
jobs: deploy: runs-on: ubuntu-latest permissions: contents: read id-token: write
steps: - uses: actions/checkout@v4
- id: auth uses: google-github-actions/auth@v2 with: workload_identity_provider: projects/${{ secrets.GCP_PROJECT_NUMBER }}/locations/global/workloadIdentityPools/github/providers/github service_account: github-deploy@${{ env.PROJECT_ID }}.iam.gserviceaccount.com
- uses: google-github-actions/setup-gcloud@v2
- name: Build and push run: | gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet docker build -t ${{ env.IMAGE }}:${{ github.sha }} . docker push ${{ env.IMAGE }}:${{ github.sha }}
- name: Run migrations run: | gcloud run jobs execute cruz-migrate \ --region=${{ env.REGION }} \ --wait \ --update-env-vars="IMAGE_TAG=${{ github.sha }}"
- name: Deploy to Cloud Run run: | gcloud run deploy ${{ env.SERVICE }} \ --image=${{ env.IMAGE }}:${{ github.sha }} \ --region=${{ env.REGION }} \ --no-traffic \ --tag=canary
gcloud run services update-traffic ${{ env.SERVICE }} \ --region=${{ env.REGION }} \ --to-latestRunning Migrations
Section titled “Running Migrations”Option 1: Cloud Run Job
Section titled “Option 1: Cloud Run Job”Create a one-off Cloud Run Job that runs cruz db migrate:
# Create the migration jobgcloud run jobs create cruz-migrate \ --image=us-central1-docker.pkg.dev/my-project/cruz-repo/cruz-app:latest \ --region=us-central1 \ --add-cloudsql-instances=my-project:us-central1:my-cruz-db \ --set-env-vars="DATABASE_URL=postgresql://cruzuser:pw@/cruzdb?host=/cloudsql/my-project:us-central1:my-cruz-db" \ --command="npx","cruz","db","migrate" \ --max-retries=0
# Run itgcloud run jobs execute cruz-migrate --region=us-central1 --waitOption 2: Startup Migration
Section titled “Option 2: Startup Migration”Add a migration step to your Dockerfile entrypoint:
#!/bin/shnpx cruz db migrate && node build/server/index.jsThis works but ties migration to instance startup, which can cause issues with multiple instances racing.
Secret Manager
Section titled “Secret Manager”Store sensitive values in Secret Manager instead of environment variables:
# Create secretsecho -n "postgresql://cruzuser:pw@/cruzdb?host=/cloudsql/..." | \ gcloud secrets create DATABASE_URL --data-file=-
echo -n "$(openssl rand -base64 32)" | \ gcloud secrets create AUTH_SECRET --data-file=-
# Grant access to the Cloud Run service accountgcloud secrets add-iam-policy-binding DATABASE_URL \ --member="serviceAccount:my-project@appspot.gserviceaccount.com" \ --role="roles/secretmanager.secretAccessor"
# Deploy with secret referencesgcloud run deploy my-cruz-app \ --update-secrets="DATABASE_URL=DATABASE_URL:latest,AUTH_SECRET=AUTH_SECRET:latest" \ ...Terraform Example
Section titled “Terraform Example”resource "google_cloud_run_v2_service" "app" { name = "my-cruz-app" location = "us-central1"
template { scaling { min_instance_count = 1 max_instance_count = 10 }
containers { image = "us-central1-docker.pkg.dev/my-project/cruz-repo/cruz-app:latest" ports { container_port = 3000 }
env { name = "GCS_BUCKET"; value = google_storage_bucket.uploads.name }
env { name = "DATABASE_URL" value_source { secret_key_ref { secret = google_secret_manager_secret.db_url.secret_id version = "latest" } } }
resources { limits = { cpu = "1", memory = "512Mi" } cpu_idle = false # cpuAlwaysAllocated } }
vpc_access { connector = google_vpc_access_connector.connector.id egress = "PRIVATE_RANGES_ONLY" }
volumes { name = "cloudsql" cloud_sql_instance { instances = [google_sql_database_instance.db.connection_name] } } }}
resource "google_sql_database_instance" "db" { name = "my-cruz-db" database_version = "POSTGRES_16" region = "us-central1"
settings { tier = "db-f1-micro" }}
resource "google_storage_bucket" "uploads" { name = "my-cruz-uploads" location = "US"}Service Accounts and IAM
Section titled “Service Accounts and IAM”The Cloud Run service account needs these roles:
| Role | Purpose |
|---|---|
roles/cloudsql.client | Connect to Cloud SQL |
roles/storage.objectAdmin | Read/write GCS |
roles/pubsub.publisher | Publish to Pub/Sub |
roles/secretmanager.secretAccessor | Read secrets |
# Create a dedicated service accountgcloud iam service-accounts create cruz-app-sa \ --display-name="CruzJS App"
# Grant rolesfor role in cloudsql.client storage.objectAdmin pubsub.publisher secretmanager.secretAccessor; do gcloud projects add-iam-policy-binding my-project \ --member="serviceAccount:cruz-app-sa@my-project.iam.gserviceaccount.com" \ --role="roles/$role"doneNext Steps
Section titled “Next Steps”- Cloud Run — runtime configuration details
- Cloud Functions — serverless deployment
- GCP Adapter Overview — service mapping and environment variables