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
| Symptom | Cause | Fix |
|---|---|---|
| Websockets disconnect every few requests | No session affinity on Cloud Run | Add --session-affinity |
| Socket connections drop after ~5 minutes | Default request timeout (300s) | Raise --timeout (for example 3600) |
| Live feature flags / realtime never update; log mentions replica set | MongoDB is standalone, not a replica set | Use Atlas or replSet — change streams required (feature-flags/src/featureFlagsApp.ts) |
| First request after idle is slow; sockets reconnect constantly | Scaled to zero | Set --min-instances=1 for user-facing services |
| Browser API calls blocked by CORS | corsOrigin / Better Auth trustedOrigins missing web origin | Add your CDN URL to backend CORS and auth config |