Skip to main content
Version: 57.2.0

Deploy backend to Cloud Run

Deploy a Terreno @terreno/api backend (for example example-backend) to Google Cloud Run with Secret Manager.

Replace placeholders: $PROJECT_ID, $REGION, $SERVICE_NAME, $REPOSITORY.

Prerequisites

  • gcloud CLI installed and authenticated
  • GCP project with billing enabled
  • MongoDB Atlas cluster (or other replica set) — deployment baseline
  • Docker (local build) or Cloud Build

Enable APIs:

gcloud services enable run.googleapis.com artifactregistry.googleapis.com secretmanager.googleapis.com --project="$PROJECT_ID"

1. Create Artifact Registry

gcloud artifacts repositories create "$REPOSITORY" \
--repository-format=docker \
--location="$REGION" \
--project="$PROJECT_ID"

2. Build and push the image

From the repository root (the Dockerfile expects the monorepo context):

docker build -f example-backend/Dockerfile -t "$REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/$SERVICE_NAME:latest" .
docker push "$REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/$SERVICE_NAME:latest"

3. Create secrets

Create Secret Manager secrets (values added out of band):

for name in mongodb-uri token-secret token-issuer refresh-token-secret session-secret; do
gcloud secrets create "$SERVICE_NAME-$name" --project="$PROJECT_ID" --replication-policy=automatic
done

Add secret values (example for MongoDB URI):

echo -n 'mongodb+srv://user:pass@cluster.example/terreno?retryWrites=true&w=majority' | \
gcloud secrets versions add "$SERVICE_NAME-mongodb-uri" --data-file=- --project="$PROJECT_ID"

Repeat for TOKEN_SECRET, TOKEN_ISSUER, REFRESH_TOKEN_SECRET, and SESSION_SECRET.

For Better Auth, also create better-auth-secret and set AUTH_PROVIDER=better-auth plus BETTER_AUTH_URL at deploy time. See Configure Better Auth.

4. Runtime service account

gcloud iam service-accounts create "$SERVICE_NAME-runtime" --project="$PROJECT_ID"

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:$SERVICE_NAME-runtime@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"

Grant object access on your uploads bucket separately if using GCS file storage.

5. Deploy to Cloud Run

gcloud run deploy "$SERVICE_NAME" \
--image="$REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/$SERVICE_NAME:latest" \
--region="$REGION" \
--project="$PROJECT_ID" \
--platform=managed \
--port=3000 \
--session-affinity \
--timeout=3600 \
--min-instances=1 \
--allow-unauthenticated \
--service-account="$SERVICE_NAME-runtime@$PROJECT_ID.iam.gserviceaccount.com" \
--set-secrets="MONGO_URI=$SERVICE_NAME-mongodb-uri:latest,TOKEN_SECRET=$SERVICE_NAME-token-secret:latest,TOKEN_ISSUER=$SERVICE_NAME-token-issuer:latest,REFRESH_TOKEN_SECRET=$SERVICE_NAME-refresh-token-secret:latest,SESSION_SECRET=$SERVICE_NAME-session-secret:latest" \
--set-env-vars="NODE_ENV=production"

Adjust min-instances to 0 for dev sandboxes (websockets will churn on cold start).

6. Verify

SERVICE_URL=$(gcloud run services describe "$SERVICE_NAME" --region="$REGION" --project="$PROJECT_ID" --format='value(status.url)')
curl -s "$SERVICE_URL/health" | jq .

Expect "healthy": true.

Production constraints

SymptomCauseFix
Websockets disconnect every few requestsNo session affinity on Cloud RunAdd --session-affinity
Socket connections drop after ~5 minutesDefault request timeout (300s)Raise --timeout (for example 3600)
Live feature flags / realtime never update; log mentions replica setMongoDB is standalone, not a replica setUse Atlas or replSet — change streams required (feature-flags/src/featureFlagsApp.ts)
First request after idle is slow; sockets reconnect constantlyScaled to zeroSet --min-instances=1 for user-facing services
Browser API calls blocked by CORScorsOrigin / Better Auth trustedOrigins missing web originAdd your CDN URL to backend CORS and auth config