Skip to content

How to: Deploy Weldall on Google Cloud

Run Weldall and the discovery proxy as Cloud Run services with Cloud SQL for PostgreSQL and Secret Manager.

This guide deploys Weldall as a Cloud Run service with one instance, stores its data in Cloud SQL for PostgreSQL and its secrets in Secret Manager, and deploys the discovery proxy as a second, independent Cloud Run service. It ends with a backup, an upgrade and a rollback, so you know the procedure before you need it.

Docker images explains the images, tags and start-up behavior this guide relies on.

Set these variables once per Cloud Shell session:

Terminal window
PROJECT=your-project-id
PROJECT_NUMBER=$(gcloud projects describe "$PROJECT" --format="value(projectNumber)")
REGION=europe-west3
WELDALL_VERSION=0.5.1
PROXY_VERSION=0.1.1
DB_TIER=db-f1-micro # database machine type for a test; for production e.g. db-custom-1-3840
gcloud config set project "$PROJECT"
Terminal window
gcloud services enable run.googleapis.com sqladmin.googleapis.com \
secretmanager.googleapis.com iam.googleapis.com
Terminal window
gcloud sql instances create weldall-db \
--region "$REGION" \
--database-version POSTGRES_16 \
--edition ENTERPRISE \
--tier "$DB_TIER"
gcloud sql databases create weldall --instance weldall-db
  • Creating the instance takes about 15 minutes.
  • The command sets only what Weldall needs: the region of the Cloud Run service, PostgreSQL 16 and a machine type. Storage, availability and automatic backups keep the Cloud SQL defaults; check them against your requirements in the instance settings.
  • db-f1-micro is a shared-core machine type for tests and not covered by the Cloud SQL SLA. For production, set DB_TIER to a dedicated-core type such as db-custom-1-3840.
  • --edition ENTERPRISE is required for these machine types; PostgreSQL 16 otherwise defaults to Enterprise Plus.

Weldall applies its migrations on every start, so its database user must be allowed to change the schema. A user created with gcloud sql users create is a member of cloudsqlsuperuser and can create and alter tables in the public schema of weldall.

The password goes directly into the connection URL in Secret Manager and is not stored anywhere else:

Terminal window
CONN="${PROJECT}:${REGION}:weldall-db"
pw=$(node -e 'process.stdout.write(require("node:crypto").randomBytes(24).toString("base64url"))')
gcloud sql users create weldall --instance weldall-db --password "$pw"
printf 'postgresql://weldall:%s@localhost/weldall?host=/cloudsql/%s' "$pw" "$CONN" \
| gcloud secrets create weldall-postgres-url \
--replication-policy user-managed --locations "$REGION" --data-file=-
unset pw

Cloud Run connects to Cloud SQL through a Unix socket under /cloudsql/. The host in the URL stays localhost; the socket path is the host parameter. The instance needs no authorized networks.

Each service runs under its own account with only the permissions it needs. The proxy needs none.

Terminal window
gcloud iam service-accounts create weldall-run --display-name "Weldall on Cloud Run"
gcloud iam service-accounts create weldall-proxy-run --display-name "Weldall discovery proxy on Cloud Run"
gcloud projects add-iam-policy-binding "$PROJECT" \
--member "serviceAccount:weldall-run@${PROJECT}.iam.gserviceaccount.com" \
--role roles/cloudsql.client --condition=None

The command generates the secrets, writes each one directly to Secret Manager without printing it, and lets only weldall-run read them:

Terminal window
tmp=$(mktemp); chmod 600 "$tmp"
node -e '
const { generateKeyPairSync, randomBytes, randomUUID } = require("node:crypto");
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
process.stdout.write(JSON.stringify({
"weldall-better-auth-secret": randomBytes(32).toString("base64url"),
"weldall-setup-token": randomBytes(32).toString("base64url"),
"weldall-credential-encryption-key": randomBytes(32).toString("base64"),
"weldall-connector-kek": randomBytes(32).toString("base64"),
"weldall-signing-private-jwk": JSON.stringify(privateKey.export({ format: "jwk" })),
"weldall-signing-public-jwk": JSON.stringify(publicKey.export({ format: "jwk" })),
"weldall-signing-kid": randomUUID(),
}));' > "$tmp"
for name in weldall-better-auth-secret weldall-setup-token weldall-credential-encryption-key \
weldall-connector-kek weldall-signing-private-jwk weldall-signing-public-jwk \
weldall-signing-kid; do
jq -j --arg k "$name" '.[$k]' "$tmp" | gcloud secrets create "$name" \
--replication-policy user-managed --locations "$REGION" --data-file=-
done
rm -f "$tmp"
for name in weldall-postgres-url weldall-better-auth-secret weldall-setup-token \
weldall-credential-encryption-key weldall-connector-kek \
weldall-signing-private-jwk weldall-signing-public-jwk weldall-signing-kid; do
gcloud secrets add-iam-policy-binding "$name" \
--member "serviceAccount:weldall-run@${PROJECT}.iam.gserviceaccount.com" \
--role roles/secretmanager.secretAccessor
done

Generate these secrets once and never replace them while this installation exists. Docker images explains why.

The URL of a Cloud Run service is known before the first deployment: https://SERVICE-PROJECT_NUMBER.REGION.run.app. Weldall needs it as WELDALL_ISSUER from the first start, because the callback URL you register at your identity provider is built from it.

Terminal window
WELDALL_URL="https://weldall-${PROJECT_NUMBER}.${REGION}.run.app"
gcloud run deploy weldall --region "$REGION" \
--image "docker.io/seibertgroup/weldall:${WELDALL_VERSION}" \
--service-account "weldall-run@${PROJECT}.iam.gserviceaccount.com" \
--port 3000 \
--max-instances 1 \
--add-cloudsql-instances "$CONN" \
--set-env-vars "WELDALL_ISSUER=${WELDALL_URL}" \
--set-secrets "POSTGRES_URL=weldall-postgres-url:latest,BETTER_AUTH_SECRET=weldall-better-auth-secret:latest,WELDALL_SETUP_TOKEN=weldall-setup-token:latest,WELDALL_CREDENTIAL_ENCRYPTION_KEY=weldall-credential-encryption-key:latest,WELDALL_CONNECTOR_KEK=weldall-connector-kek:latest,WELDALL_SIGNING_PRIVATE_JWK=weldall-signing-private-jwk:latest,WELDALL_SIGNING_PUBLIC_JWK=weldall-signing-public-jwk:latest,WELDALL_SIGNING_KID=weldall-signing-kid:latest" \
--liveness-probe "httpGet.path=/.well-known/openid-configuration,httpGet.port=3000,periodSeconds=30,timeoutSeconds=5,failureThreshold=3" \
--allow-unauthenticated
  • The command sets only what Weldall needs. CPU, memory and minimum instances keep the Cloud Run defaults; adjust them to your requirements, see the gcloud run deploy reference.
  • --port 3000 is the port the Weldall image listens on.
  • --max-instances 1 keeps Weldall to one instance; Docker images explains why. The limit applies per revision: during a deployment, the old and the new revision run side by side for a short time.
  • --liveness-probe makes Cloud Run restart an instance that hangs. Without it, Cloud Run only notices a process that exits.
  • --allow-unauthenticated makes Weldall reachable for browsers, the CLI and resource servers. Cloud Run provides the HTTPS certificate for the run.app URL. The installer stays closed to anyone without the setup token.
  • Cloud Run pulls public Docker Hub images without credentials and pins the image digest per revision. A moving tag such as 0.5 or latest therefore does not change a running service; only the next gcloud run deploy resolves it again.
  • By default, Cloud Run stops the instance when idle. The first request afterwards waits for a cold start of a few seconds. --min-instances 1 avoids that and is billed continuously.
Terminal window
curl -s "$WELDALL_URL/.well-known/openid-configuration" | jq -r .issuer # prints $WELDALL_URL
curl -s "$WELDALL_URL/api/me/skills"; echo # invalid_token
gcloud logging read \
'resource.type="cloud_run_revision" AND resource.labels.service_name="weldall"' \
--order=asc --limit 200 --format="value(textPayload,jsonPayload.message)" \
| grep -E "migrations found|Applying migration|successfully applied|No pending|Deployment initialization|server initialized"

The second call answers with invalid_token. That is the expected result: the API requires a login.

On the first start, the log shows every migration being applied, All migrations have been successfully applied., then Deployment initialization completed, then Weldall server initialized.

Open $WELDALL_URL and follow How to: Run the installer. Copy the setup token to the clipboard without printing it, for example in a local terminal on macOS:

Terminal window
gcloud secrets versions access latest --secret weldall-setup-token --project "$PROJECT" | pbcopy

Skip this step if all your resource servers can reach $WELDALL_URL directly.

Terminal window
gcloud run deploy weldall-discovery-proxy --region "$REGION" \
--image "docker.io/seibertgroup/weldall-discovery-proxy:${PROXY_VERSION}" \
--service-account "weldall-proxy-run@${PROJECT}.iam.gserviceaccount.com" \
--set-env-vars "WELDALL_UPSTREAM=${WELDALL_URL}" \
--allow-unauthenticated

Everything else keeps the Cloud Run defaults, including port 8080, which is the port the proxy listens on.

Verify that the proxy forwards both documents unchanged and blocks everything else:

Terminal window
PROXY_URL="https://weldall-discovery-proxy-${PROJECT_NUMBER}.${REGION}.run.app"
cmp <(curl -s "$WELDALL_URL/.well-known/oauth-authorization-server") \
<(curl -s "$PROXY_URL/.well-known/oauth-authorization-server") && echo identical
cmp <(curl -s "$WELDALL_URL/api/oauth/jwks") <(curl -s "$PROXY_URL/api/oauth/jwks") && echo identical
curl -s -o /dev/null -w "%{http_code}\n" "$PROXY_URL/.well-known/openid-configuration" # 404
curl -s -o /dev/null -w "%{http_code}\n" -X POST "$PROXY_URL/api/oauth/jwks" # 405

Resource servers use $PROXY_URL as discoveryProxyOrigin and keep $WELDALL_URL as the issuer, see Discovery proxy.

Take a backup directly before the upgrade and note its ID:

Terminal window
NEW_VERSION=0.5.2 # the version you upgrade to
gcloud sql backups create --instance weldall-db --description "before Weldall ${NEW_VERSION}"
gcloud sql backups list --instance weldall-db --limit 3

Deploy the new version. Only the image changes; Cloud Run keeps every other setting of the previous revision:

Terminal window
gcloud run deploy weldall --region "$REGION" \
--image "docker.io/seibertgroup/weldall:${NEW_VERSION}"

The log of the new revision shows only the new migrations, then the initialization and the server start. Run the checks from step 7 again.

Starting the previous image alone is not enough. The older version starts without a warning and reports No pending migrations to apply., while the database keeps the schema of the newer version. Docker images explains the consequences.

Restore the backup from step 9, then start the previous image:

Terminal window
gcloud sql backups restore BACKUP_ID --restore-instance weldall-db --backup-instance weldall-db
gcloud run deploy weldall --region "$REGION" \
--image "docker.io/seibertgroup/weldall:${WELDALL_VERSION}"
  • The restore replaces the whole instance with the state of the backup. Logins, administration changes and audit records written after the backup are lost.
  • The database is unavailable during the restore, which takes several minutes. Plan a maintenance window.

The proxy stores nothing and is independent of Weldall. Upgrade it with a new image, and roll it back by sending the traffic to the previous revision:

Terminal window
NEW_PROXY_VERSION=0.1.2 # the version you upgrade to
gcloud run deploy weldall-discovery-proxy --region "$REGION" \
--image "docker.io/seibertgroup/weldall-discovery-proxy:${NEW_PROXY_VERSION}"
gcloud run revisions list --service weldall-discovery-proxy --region "$REGION"
gcloud run services update-traffic weldall-discovery-proxy --region "$REGION" \
--to-revisions PREVIOUS_REVISION=100

After --to-revisions, the traffic stays pinned to that revision, and later deployments receive none. Undo the pin before the next upgrade:

Terminal window
gcloud run services update-traffic weldall-discovery-proxy --region "$REGION" --to-latest

Do not roll back Weldall this way: it would reactivate an old revision without restoring the database.