Google Cloud Run
Deploy Stategraph as a fully managed Google Cloud Run service, with Cloud SQL for PostgreSQL through a Cloud SQL Auth Proxy sidecar, and Secret Manager for the database password.
Before you begin
- The
gcloudCLI, authenticated withgcloud auth login - A Google Cloud project with billing enabled
- Owner or Editor on the project, or the equivalent roles for Cloud Run, Cloud SQL, Secret Manager, and Artifact Registry
- Docker, to mirror the image into Artifact Registry
Cloud Run pulls only from Artifact Registry, Container Registry, or Docker Hub
Cloud Run does not deploy from ghcr.io. Mirror ghcr.io/stategraph/stategraph-server one time into an Artifact Registry repository in your project, and point the service at the *-docker.pkg.dev path.
Architecture
server container on port 8080.The server connects to the
cloud-sql-proxy sidecar on 127.0.0.1:5432, and the sidecar connects to Cloud SQL for PostgreSQL.The database password comes from Secret Manager.
Quick start
The commands use these shell variables:
export PROJECT_ID="your-project-id"
export REGION="us-central1"
export INSTANCE="stategraph-db"
export AR_REPO="stategraph"
gcloud config set project "$PROJECT_ID"
gcloud config set run/region "$REGION"
1. Enable APIs
gcloud services enable \
run.googleapis.com \
sqladmin.googleapis.com \
secretmanager.googleapis.com \
artifactregistry.googleapis.com \
compute.googleapis.com
2. Mirror the image into Artifact Registry
gcloud artifacts repositories create "$AR_REPO" \
--repository-format=docker \
--location="$REGION" \
--description="Stategraph server images"
gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet
export IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO}/stategraph-server:2.5.7"
docker pull ghcr.io/stategraph/stategraph-server:2.5.7
docker tag ghcr.io/stategraph/stategraph-server:2.5.7 "$IMAGE"
docker push "$IMAGE"
3. Create the Cloud SQL database
gcloud sql instances create "$INSTANCE" \
--database-version=POSTGRES_17 \
--edition=ENTERPRISE \
--tier=db-custom-1-3840 \
--region="$REGION" \
--storage-size=10 \
--storage-type=SSD
DB_PASSWORD="$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)"
gcloud sql databases create stategraph --instance="$INSTANCE"
gcloud sql users create stategraph --instance="$INSTANCE" --password="$DB_PASSWORD"
export CONNECTION_NAME="$(gcloud sql instances describe "$INSTANCE" --format='value(connectionName)')"
echo "$CONNECTION_NAME"
Tier and edition
POSTGRES_17 defaults to the Enterprise Plus edition, which rejects the small shared-core tiers. --edition=ENTERPRISE allows tiers such as db-custom-1-3840 (1 vCPU, 3.75 GB), or the shared-core db-g1-small for evaluation. Use a larger tier for production.
4. Store the database password in Secret Manager
printf '%s' "$DB_PASSWORD" | gcloud secrets create stategraph-db-pass --data-file=-
5. Grant the runtime service account access
Cloud Run runs as the project default compute service account, unless you choose another:
export PROJECT_NUMBER="$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')"
export RUNTIME_SA="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${RUNTIME_SA}" \
--role="roles/cloudsql.client" --condition=None
gcloud secrets add-iam-policy-binding stategraph-db-pass \
--member="serviceAccount:${RUNTIME_SA}" \
--role="roles/secretmanager.secretAccessor"
gcloud artifacts repositories add-iam-policy-binding "$AR_REPO" \
--location="$REGION" \
--member="serviceAccount:${RUNTIME_SA}" \
--role="roles/artifactregistry.reader"
6. Write the service definition
Save this as service.yaml, with your values for IMAGE and CONNECTION_NAME:
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: stategraph
labels:
cloud.googleapis.com/location: us-central1
spec:
template:
metadata:
annotations:
# Keep one warm instance: the price-book loader and the scheduled
# jobs are background work that scale-to-zero would interrupt.
autoscaling.knative.dev/minScale: "1"
autoscaling.knative.dev/maxScale: "3"
# Always allocate CPU so background work runs between requests.
run.googleapis.com/cpu-throttling: "false"
run.googleapis.com/startup-cpu-boost: "true"
# Start the proxy before the server so the database is reachable at boot.
run.googleapis.com/container-dependencies: '{"server":["cloud-sql-proxy"]}'
spec:
containers:
- name: server
image: IMAGE
ports:
- containerPort: 8080
env:
- name: DB_HOST
value: "127.0.0.1"
- name: DB_PORT
value: "5432"
- name: DB_USER
value: "stategraph"
- name: DB_NAME
value: "stategraph"
- name: DB_PASS
valueFrom:
secretKeyRef:
name: stategraph-db-pass
key: "latest"
# Set to the service URL after the first deploy (step 8).
- name: STATEGRAPH_UI_BASE
value: "https://REPLACE_WITH_SERVICE_URL"
# Migrations run at boot; /health/ready returns 502 until they finish.
startupProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 30
timeoutSeconds: 5
resources:
limits:
cpu: "1"
memory: 1Gi
- name: cloud-sql-proxy
image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.11.0
args:
- "--port=5432"
- "--health-check"
- "--http-address=0.0.0.0"
- "--http-port=9090"
- "CONNECTION_NAME"
startupProbe:
httpGet:
path: /startup
port: 9090
periodSeconds: 5
failureThreshold: 12
timeoutSeconds: 3
resources:
limits:
cpu: "1"
memory: 512Mi
traffic:
- percent: 100
latestRevision: true
7. Deploy
gcloud run services replace service.yaml --region="$REGION"
The deploy completes when the server startup probe passes: /health/ready returns 200 after the migrations, and Cloud Run sends no traffic before then. With failureThreshold: 30 and periodSeconds: 10, the migrations have five minutes. For a large database, raise the threshold. See Health checks.
8. Set the public URL and redeploy
Cloud Run assigns the service URL at the first deploy. Set it in STATEGRAPH_UI_BASE, which Stategraph uses for links, cookies, and sign-in callbacks:
export SERVICE_URL="$(gcloud run services describe stategraph --region="$REGION" --format='value(status.url)')"
echo "$SERVICE_URL"
sed -i "s#https://REPLACE_WITH_SERVICE_URL#${SERVICE_URL}#" service.yaml
gcloud run services replace service.yaml --region="$REGION"
The Cloud Run URL is always https://, so cookies have the Secure flag.
9. Expose the console
To open the console in a browser, allow unauthenticated access:
gcloud run services add-iam-policy-binding stategraph \
--region="$REGION" \
--member=allUsers \
--role=roles/run.invoker
To keep the service private, skip this step and use an identity token or Identity-Aware Proxy.
Domain restricted sharing
If your organization enforces iam.allowedPolicyMemberDomains, it rejects the allUsers binding. Grant the project an allowAll override for that constraint, or grant roles/run.invoker to specific users or groups.
10. Verify the deployment
curl -sS -o /dev/null -w "live: %{http_code}\n" "$SERVICE_URL/health/live"
curl -sS -o /dev/null -w "ready: %{http_code}\n" "$SERVICE_URL/health/ready"
If the service requires authentication, add an identity token:
TOKEN="$(gcloud auth print-identity-token)"
curl -sS -H "Authorization: Bearer $TOKEN" \
-o /dev/null -w "ready: %{http_code}\n" "$SERVICE_URL/health/ready"
Open $SERVICE_URL and create the first admin account on the setup screen.
Configuration
The quick start sets the minimum on the server container:
STATEGRAPH_UI_BASE: the service URL, for examplehttps://stategraph-xxxx.us-central1.run.app, or your custom domain.DB_HOSTandDB_PORT: the sidecar,127.0.0.1:5432, over TCP, so no database IP is exposed.DB_USERandDB_NAME:stategraph.DB_PASS: the database password, from Secret Manager.
For all other settings, see Environment variables.
Custom domain
Map a domain to the service, then set STATEGRAPH_UI_BASE to it and redeploy:
gcloud run domain-mappings create \
--service=stategraph \
--domain=stategraph.example.com \
--region="$REGION"
Authentication
Local email and password sign-in is on by default. For Google or OIDC, add the OAuth variables to the server container. The callback path is /oauth2/{provider}/callback, so STATEGRAPH_OAUTH_REDIRECT_BASE must equal STATEGRAPH_UI_BASE:
- name: STATEGRAPH_OAUTH_TYPE
value: "google"
- name: STATEGRAPH_OAUTH_CLIENT_ID
value: "your-client-id.apps.googleusercontent.com"
- name: STATEGRAPH_OAUTH_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: stategraph-oauth-secret
key: "latest"
- name: STATEGRAPH_OAUTH_REDIRECT_BASE
value: "https://stategraph.example.com"
- name: STATEGRAPH_OAUTH_EMAIL_DOMAIN
value: "example.com"
In your provider, register https://<your-domain>/oauth2/google/callback or /oauth2/oidc/callback. Keep the client secret in Secret Manager, and grant the runtime service account roles/secretmanager.secretAccessor on it, as for the database password. See Access control.
Enable cost estimation
Add STATEGRAPH_COST_ENABLED=true to the server container, and point PRICING_DB_* at Cloud SQL. The defaults, host db and password stategraph, cannot reach it, so with only the switch, cost never works.
- name: STATEGRAPH_COST_ENABLED
value: "true"
# The pricing service reads PRICING_DB_*, not DB_*. Reach Cloud SQL through
# the same sidecar with the same password. PRICING_DB_USER and PRICING_DB_NAME
# already default to stategraph and cloud_pricing.
- name: PRICING_DB_HOST
value: "127.0.0.1"
- name: PRICING_DB_PASSWORD
valueFrom:
secretKeyRef:
name: stategraph-db-pass
key: "latest"
At the first start, Stategraph creates the cloud_pricing database, because the gcloud user has cloudsqlsuperuser, which includes CREATEDB. It then loads the price book in the background. The price book is in Cloud SQL, so it survives instance replacement. See Enable cost estimation.
Enable Orchestration
Stategraph Orchestration is off by default. To turn it on, add STATEGRAPH_ORCHESTRATION_ENABLED=true to the server container. Also add the Orchestration public URLs, the FDW role password, and the GitHub App or GitLab credentials from Secret Manager. GitLab also needs STATEGRAPH_FDW_PROVISIONER_PASSWORD, so that the console can connect a GitLab group.
Orchestration also needs the terrateam database on the same Cloud SQL instance:
- The
gclouduser hasCREATEDB, so Orchestration creates it at the first start, owned by the role that it connects as. - The
stategraphdatabase reads it throughpostgres_fdw, which on managed PostgreSQL needs superuser rights, or a foreign server and user mapping that you create in advance. STATEGRAPH_FDW_HOSTis the address that the Cloud SQL instance uses to reachterrateam, not the sidecar address.
Enable Orchestration lists the variables and the roles.
Scaling
Cloud Run scales on request concurrency, within the revision template bounds:
autoscaling.knative.dev/minScale: "1"
autoscaling.knative.dev/maxScale: "3"
Keep minScale at 1 or higher: scale-to-zero stops the price-book load and the scheduled recomputes. run.googleapis.com/cpu-throttling: "false" lets them run between requests. To scale vertically, raise the server CPU and memory limits, for example cpu: "2" and memory: 2Gi.
Upgrading
Mirror the new tag into Artifact Registry, update image: in service.yaml, and redeploy:
docker pull ghcr.io/stategraph/stategraph-server:2.5.7
docker tag ghcr.io/stategraph/stategraph-server:2.5.7 "${REGION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO}/stategraph-server:2.5.7"
docker push "${REGION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO}/stategraph-server:2.5.7"
gcloud run services replace service.yaml --region="$REGION"
Cloud Run moves traffic to the new revision when its startup probe passes. See Upgrades.
Monitoring
gcloud run services logs read stategraph --region="$REGION" --limit=100
gcloud run services describe stategraph --region="$REGION"
In the Cloud console, Cloud Run > stategraph > Metrics shows request count, latency, instance count, CPU, and memory. For other signals, see Observability.
Troubleshooting
Deploy rejected: invalid image host
Symptoms: gcloud run services replace fails on the image reference.
Error message
Expected an image path like [host/]repo-path[:tag and/or @digest], where host is
one of [region.]gcr.io, [region-]docker.pkg.dev or docker.io
Solution:
- Cloud Run cannot pull from
ghcr.io. Mirror the image (step 2), and use the*-docker.pkg.devpath
Cloud SQL instance create fails on tier
Symptoms: gcloud sql instances create fails at once.
Error message
Invalid Tier (db-f1-micro) for (ENTERPRISE_PLUS) Edition
Solution:
- Add
--edition=ENTERPRISEand a supported tier. See the note in step 3
Service stuck starting or readiness never passes
Symptoms: the deploy times out, or /health/ready keeps returning 502.
Solutions:
- Check that the runtime service account has
roles/cloudsql.clientandroles/secretmanager.secretAccessor - Read the
cloud-sql-proxycontainer log for a wrongCONNECTION_NAMEor missing IAM - For a large database, raise the
failureThresholdof theserverstartup probe - Read the
servercontainer log for a sign-in provider error, which stops the start