14 KiB
Cloud Build Environment Configuration
This guide explains how to set up separate staging and production triggers with environment-specific configurations.
Overview
The cloudbuild.yaml uses substitution variables to make it environment-agnostic. You'll create two separate triggers (one for staging, one for production) with different substitution variable values.
Note: The Dockerfile uses a single final stage. Environment differentiation (staging vs production) is handled entirely through Cloud Run environment variables, not at build time. This simplifies the build process - the same Docker image can be used for both environments.
Architecture
┌─────────────────┐ ┌──────────────────────┐
│ Git Branch │ │ Cloud Build Trigger │
│ - main │─────▶│ (Production) │
│ │ │ Variables: prod │
└─────────────────┘ └──────────────────────┘
┌─────────────────┐ ┌──────────────────────┐
│ Git Branch │ │ Cloud Build Trigger │
│ - develop │─────▶│ (Staging) │
│ │ │ Variables: staging │
└─────────────────┘ └──────────────────────┘
Required Substitution Variables
Common Variables (Different per Environment)
| Variable | Production Value | Staging Value |
|---|---|---|
$_NODE_ENV |
production |
staging |
$_SERVICE_NAME |
xtablo-api |
xtablo-api-staging |
$_SUPABASE_URL |
Production Supabase URL | Staging Supabase URL |
$_STREAM_CHAT_API_KEY |
Production key | Staging key |
$_EMAIL_USER |
noreply@xtablo.com |
staging@xtablo.com |
$_EMAIL_CLIENT_ID |
Production OAuth client | Staging OAuth client |
$_R2_ACCOUNT_ID |
Production R2 account | Staging R2 account |
$_CORS_ORIGIN |
https://app.xtablo.com |
https://staging.xtablo.com |
$_XTABLO_URL |
https://app.xtablo.com |
https://staging.xtablo.com |
$_LOG_LEVEL |
info |
debug |
Build Configuration (Same for Both)
| Variable | Value |
|---|---|
$_AR_PROJECT_ID |
Your GCP project ID |
$_AR_REPOSITORY |
xtablo (or your repository name) |
Secret Manager References (Different per Environment)
Each environment should have its own secrets in Secret Manager:
| Variable | Production Secret Name | Staging Secret Name |
|---|---|---|
$_SUPABASE_SERVICE_ROLE_KEY_SECRET |
supabase-service-role-key-prod |
supabase-service-role-key-staging |
$_SUPABASE_CONNECTION_STRING_SECRET |
supabase-connection-string-prod |
supabase-connection-string-staging |
$_SUPABASE_CA_CERT_SECRET |
supabase-ca-cert-prod |
supabase-ca-cert-staging |
$_STREAM_CHAT_API_SECRET_SECRET |
stream-chat-api-secret-prod |
stream-chat-api-secret-staging |
$_STRIPE_SECRET_KEY_SECRET |
stripe-secret-key-prod |
stripe-secret-key-staging |
$_STRIPE_WEBHOOK_SECRET_SECRET |
stripe-webhook-secret-prod |
stripe-webhook-secret-staging |
$_EMAIL_CLIENT_SECRET_SECRET |
email-client-secret-prod |
email-client-secret-staging |
$_EMAIL_REFRESH_TOKEN_SECRET |
email-refresh-token-prod |
email-refresh-token-staging |
$_R2_ACCESS_KEY_ID_SECRET |
r2-access-key-id-prod |
r2-access-key-id-staging |
$_R2_SECRET_ACCESS_KEY_SECRET |
r2-secret-access-key-prod |
r2-secret-access-key-staging |
Setup Instructions
Step 1: Create Secrets in Secret Manager
Production Secrets
# Supabase (Production)
echo -n "prod-service-role-key" | gcloud secrets create supabase-service-role-key-prod --data-file=-
echo -n "prod-connection-string" | gcloud secrets create supabase-connection-string-prod --data-file=-
echo -n "prod-ca-cert" | gcloud secrets create supabase-ca-cert-prod --data-file=-
# Stream Chat (Production)
echo -n "prod-stream-secret" | gcloud secrets create stream-chat-api-secret-prod --data-file=-
# Stripe (Production)
echo -n "prod-stripe-key" | gcloud secrets create stripe-secret-key-prod --data-file=-
echo -n "prod-webhook-secret" | gcloud secrets create stripe-webhook-secret-prod --data-file=-
# Email (Production)
echo -n "prod-client-secret" | gcloud secrets create email-client-secret-prod --data-file=-
echo -n "prod-refresh-token" | gcloud secrets create email-refresh-token-prod --data-file=-
# R2 (Production)
echo -n "prod-access-key-id" | gcloud secrets create r2-access-key-id-prod --data-file=-
echo -n "prod-secret-access-key" | gcloud secrets create r2-secret-access-key-prod --data-file=-
Staging Secrets
# Supabase (Staging)
echo -n "staging-service-role-key" | gcloud secrets create supabase-service-role-key-staging --data-file=-
echo -n "staging-connection-string" | gcloud secrets create supabase-connection-string-staging --data-file=-
echo -n "staging-ca-cert" | gcloud secrets create supabase-ca-cert-staging --data-file=-
# Stream Chat (Staging)
echo -n "staging-stream-secret" | gcloud secrets create stream-chat-api-secret-staging --data-file=-
# Stripe (Staging - use test keys)
echo -n "staging-stripe-key" | gcloud secrets create stripe-secret-key-staging --data-file=-
echo -n "staging-webhook-secret" | gcloud secrets create stripe-webhook-secret-staging --data-file=-
# Email (Staging)
echo -n "staging-client-secret" | gcloud secrets create email-client-secret-staging --data-file=-
echo -n "staging-refresh-token" | gcloud secrets create email-refresh-token-staging --data-file=-
# R2 (Staging)
echo -n "staging-access-key-id" | gcloud secrets create r2-access-key-id-staging --data-file=-
echo -n "staging-secret-access-key" | gcloud secrets create r2-secret-access-key-staging --data-file=-
Step 2: Grant Access to Secrets
PROJECT_ID="your-project-id"
SERVICE_ACCOUNT="${PROJECT_ID}@appspot.gserviceaccount.com"
# Grant access to all production secrets
for secret in \
supabase-service-role-key-prod \
supabase-connection-string-prod \
supabase-ca-cert-prod \
stream-chat-api-secret-prod \
stripe-secret-key-prod \
stripe-webhook-secret-prod \
email-client-secret-prod \
email-refresh-token-prod \
r2-access-key-id-prod \
r2-secret-access-key-prod
do
gcloud secrets add-iam-policy-binding $secret \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/secretmanager.secretAccessor"
done
# Grant access to all staging secrets
for secret in \
supabase-service-role-key-staging \
supabase-connection-string-staging \
supabase-ca-cert-staging \
stream-chat-api-secret-staging \
stripe-secret-key-staging \
stripe-webhook-secret-staging \
email-client-secret-staging \
email-refresh-token-staging \
r2-access-key-id-staging \
r2-secret-access-key-staging
do
gcloud secrets add-iam-policy-binding $secret \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/secretmanager.secretAccessor"
done
Step 3: Create Cloud Build Triggers
Production Trigger
Via gcloud CLI:
gcloud builds triggers create github \
--name="xtablo-api-production" \
--repo-name="xtablo-source" \
--repo-owner="your-github-org" \
--branch-pattern="^main$" \
--build-config="apps/api/cloudbuild.yaml" \
--substitutions='
_NODE_ENV=production,
_SERVICE_NAME=xtablo-api,
_AR_PROJECT_ID=your-project-id,
_AR_REPOSITORY=xtablo,
_SUPABASE_URL=https://your-prod-project.supabase.co,
_STREAM_CHAT_API_KEY=your-prod-stream-key,
_EMAIL_USER=noreply@xtablo.com,
_EMAIL_CLIENT_ID=your-prod-oauth-client-id,
_R2_ACCOUNT_ID=your-prod-r2-account,
_CORS_ORIGIN=https://app.xtablo.com,
_XTABLO_URL=https://app.xtablo.com,
_LOG_LEVEL=info,
_SUPABASE_SERVICE_ROLE_KEY_SECRET=supabase-service-role-key-prod,
_SUPABASE_CONNECTION_STRING_SECRET=supabase-connection-string-prod,
_SUPABASE_CA_CERT_SECRET=supabase-ca-cert-prod,
_STREAM_CHAT_API_SECRET_SECRET=stream-chat-api-secret-prod,
_STRIPE_SECRET_KEY_SECRET=stripe-secret-key-prod,
_STRIPE_WEBHOOK_SECRET_SECRET=stripe-webhook-secret-prod,
_EMAIL_CLIENT_SECRET_SECRET=email-client-secret-prod,
_EMAIL_REFRESH_TOKEN_SECRET=email-refresh-token-prod,
_R2_ACCESS_KEY_ID_SECRET=r2-access-key-id-prod,
_R2_SECRET_ACCESS_KEY_SECRET=r2-secret-access-key-prod
'
Staging Trigger
Via gcloud CLI:
gcloud builds triggers create github \
--name="xtablo-api-staging" \
--repo-name="xtablo-source" \
--repo-owner="your-github-org" \
--branch-pattern="^develop$" \
--build-config="apps/api/cloudbuild.yaml" \
--substitutions='
_NODE_ENV=staging,
_SERVICE_NAME=xtablo-api-staging,
_AR_PROJECT_ID=your-project-id,
_AR_REPOSITORY=xtablo,
_SUPABASE_URL=https://your-staging-project.supabase.co,
_STREAM_CHAT_API_KEY=your-staging-stream-key,
_EMAIL_USER=staging@xtablo.com,
_EMAIL_CLIENT_ID=your-staging-oauth-client-id,
_R2_ACCOUNT_ID=your-staging-r2-account,
_CORS_ORIGIN=https://staging.xtablo.com,
_XTABLO_URL=https://staging.xtablo.com,
_LOG_LEVEL=debug,
_SUPABASE_SERVICE_ROLE_KEY_SECRET=supabase-service-role-key-staging,
_SUPABASE_CONNECTION_STRING_SECRET=supabase-connection-string-staging,
_SUPABASE_CA_CERT_SECRET=supabase-ca-cert-staging,
_STREAM_CHAT_API_SECRET_SECRET=stream-chat-api-secret-staging,
_STRIPE_SECRET_KEY_SECRET=stripe-secret-key-staging,
_STRIPE_WEBHOOK_SECRET_SECRET=stripe-webhook-secret-staging,
_EMAIL_CLIENT_SECRET_SECRET=email-client-secret-staging,
_EMAIL_REFRESH_TOKEN_SECRET=email-refresh-token-staging,
_R2_ACCESS_KEY_ID_SECRET=r2-access-key-id-staging,
_R2_SECRET_ACCESS_KEY_SECRET=r2-secret-access-key-staging
'
Step 4: Configure via Console (Alternative)
If you prefer using the Google Cloud Console:
-
Go to Cloud Build > Triggers
-
Click Create Trigger
-
Configure:
- Name:
xtablo-api-production(orxtablo-api-staging) - Event: Push to a branch
- Repository: Your GitHub repository
- Branch:
^main$(or^develop$for staging) - Build configuration: Cloud Build configuration file
- Location:
apps/api/cloudbuild.yaml
- Name:
-
Scroll to Substitution variables and add all variables from the table above
-
Save the trigger
Verification
Test Production Deployment
# Trigger manually
gcloud builds triggers run xtablo-api-production --branch=main
# Check logs
gcloud builds list --limit=5
gcloud builds log BUILD_ID
# Verify deployment
gcloud run services describe xtablo-api --region=europe-west1
Test Staging Deployment
# Trigger manually
gcloud builds triggers run xtablo-api-staging --branch=develop
# Check logs
gcloud builds list --limit=5
# Verify deployment
gcloud run services describe xtablo-api-staging --region=europe-west1
Verify Environment Variables
# Production
gcloud run services describe xtablo-api --region=europe-west1 \
--format="value(spec.template.spec.containers[0].env)"
# Staging
gcloud run services describe xtablo-api-staging --region=europe-west1 \
--format="value(spec.template.spec.containers[0].env)"
Best Practices
- Secret Naming: Use
-prodand-stagingsuffixes for secrets - Service Names: Use different names for each environment (
xtablo-apivsxtablo-api-staging) - Branches: Deploy production from
main, staging fromdevelop - Stripe Keys: Use Stripe test keys in staging
- Supabase: Use separate Supabase projects for staging and production
- Monitoring: Set up different alert thresholds for staging vs production
- Resource Limits: You may want lower limits for staging to save costs
Quick Reference
Environment Comparison
| Aspect | Production | Staging |
|---|---|---|
| Branch | main |
develop |
| Service Name | xtablo-api |
xtablo-api-staging |
| Domain | app.xtablo.com |
staging.xtablo.com |
| Log Level | info |
debug |
| Stripe | Live keys | Test keys |
| Min Instances | 1 (optional) | 0 |
| Secrets Suffix | -prod |
-staging |
Troubleshooting
"Secret not found" error
- Ensure secret names in substitution variables match actual secret names
- Verify secrets exist:
gcloud secrets list | grep stagingorgrep prod
Wrong environment variables in deployment
- Check trigger substitution variables in Cloud Build console
- Verify the correct trigger is being used for the branch
Services pointing to wrong databases
- Ensure
SUPABASE_URLis different for staging/production - Check that secret names include correct environment suffix
Example: Adding a New Environment Variable
To add a new environment variable (e.g., FEATURE_FLAG):
-
Add to
cloudbuild.yamlin the--set-env-varsline:"NODE_ENV=$_NODE_ENV,...,FEATURE_FLAG=$_FEATURE_FLAG" -
Add to both triggers with different values:
- Production:
_FEATURE_FLAG=false - Staging:
_FEATURE_FLAG=true
- Production:
-
Update app code to read
process.env.FEATURE_FLAG