Keycloak on Cloud Run

Terraform and a container build for running Keycloak 26 on Google Cloud Run, behind a global external Application Load Balancer, with Cloud SQL for PostgreSQL.

Apache-2.0. Image published to ghcr.io/bcollard/keycloak-cloudrun, signed with cosign.

What you get

A production-shaped Keycloak with no VMs or Kubernetes cluster to run.

Serverless Keycloak

Cloud Run runs an optimized image (kc.sh start --optimized) with health, metrics and the PostgreSQL driver built in.

Private database

Cloud SQL for PostgreSQL on a private IP only, reached through a Serverless VPC Access connector.

Global load balancer

Static IP, TLS on 443, HTTP-to-HTTPS redirect, and a URL map that only exposes Keycloak's public paths.

Gated admin console

/admin is forwarded only when the request carries a shared-secret header. Cloud Run ingress blocks the *.run.app URL.

Signed image

GitHub Actions builds a multi-arch image, pushes it to GHCR and signs it with cosign. Cloud Run deploys it by digest through an Artifact Registry proxy.

Secrets in Secret Manager

Database and bootstrap admin passwords are stored in Secret Manager and injected as environment variables.

Architecture

How a request reaches Keycloak.

  1. DNS resolves the Keycloak hostname to a reserved global static IP.
  2. Port 443 terminates TLS with an uploaded certificate. Port 80 returns a 301 to HTTPS.
  3. The URL map sends /realms/, /.well-known/, /resources/ and /js/ to the backend. /admin needs the header. Everything else is redirected.
  4. The backend service reaches Cloud Run through a serverless NEG.
  5. Keycloak reaches Cloud SQL over the VPC Access connector.
Keycloak on Cloud Run architecture: global load balancer, serverless NEG, Cloud Run, VPC Access connector and Cloud SQL

What the stack includes

Who creates what.

OwnerResources
TerraformStatic IP, forwarding rules, HTTPS and HTTP proxies, URL maps, SSL certificate, backend service, serverless NEG, Cloud Run service, service account and IAM, Cloud SQL, VPC Access connector, Secret Manager secrets, Artifact Registry remote repository, project APIs
GitHub ActionsMulti-arch image build, push to GHCR, cosign signing, terraform validate, Renovate
You, onceGCS state bucket, private services access on the VPC, DNS A record, TLS certificate, realm configuration
Resources created by Terraform, by GitHub Actions, and by hand

Deploy

Requires a GCP project with billing, Terraform 1.10 or later, a GCS bucket for state, and private services access on the default VPC.

Configure

git clone https://github.com/bcollard/keycloak-cloudrun
cd keycloak-cloudrun
cp backend.hcl.example backend.hcl               # state bucket and prefix
cp terraform.tfvars.example values.auto.tfvars   # project, hostname, passwords

Get a TLS certificate

The load balancer uses an uploaded certificate. Any publicly trusted certificate that covers your Keycloak hostname works, single name or wildcard, from any CA or ACME client. Set ssl_certificate_path (full chain, PEM) and ssl_certificate_key_path (private key, PEM) in values.auto.tfvars.

# lego with your DNS provider, see https://go-acme.github.io/lego/dns/
lego run --email me@example.com --dns <provider> -d keycloak.example.com --accept-tos --path ./.lego

# certbot with a manual DNS-01 TXT record
certbot certonly --manual --preferred-challenges dns -d keycloak.example.com

If your zone is on Google Cloud DNS, make server-cert wraps lego and issues a wildcard certificate:

export DNS_ZONE_NAME=example.com GCP_PROJECT_DNS=my-dns-project \
       GCP_CREDENTIALS_FILE=/path/to/dns-sa.json ACME_EMAIL=me@example.com
make server-cert   # writes .lego/certificates/_.example.com.{crt,key}

Apply

make gcloud-login GCLOUD_CONFIG=my-config   # activate the gcloud config and refresh ADC for Terraform
make tfi                                    # terraform init with backend.hcl
make tfp                                    # terraform plan, review the changes
make tfa                                    # terraform apply

Create the DNS record

At your DNS provider, create an A record for the Keycloak hostname that points at the load balancer IP:

terraform output -raw forwarding_rule_ip_address

Keycloak features enabled

Preview and experimental features for agent and workload identity, baked in at build time.

FlagStatus in 26.8Purpose
cimd:v1experimentalOAuth Client ID Metadata Document, required by MCP clients since spec 2025-11-25
identity-assertion-jwt:v1experimentalIdentity Assertion JWT Authorization Grant (ID-JAG)
spiffe:v1previewSPIFFE trust relationship provider
parameterized-scopes:v1previewParameterized OAuth 2.0 client scopes
client-admin-api:v2previewClient Admin API v2

Standard token exchange v2, JWT authorization grant and Kubernetes service-account grants are on by default since Keycloak 26.6.

FAQ

Does CI deploy to Cloud Run?
No. GitHub Actions builds and signs the image when KEYCLOAK_VERSION changes. You run terraform apply yourself.
How do I upgrade Keycloak?
Bump KEYCLOAK_VERSION and push. When the build finishes, set keycloak_version and keycloak_image_digest (from make image-digest) and run make tfa.
Can the certificate be rotated without downtime?
Yes. The certificate uses name_prefix and create_before_destroy, so the proxy switches to the new one before the old one is deleted.
Why does the revision fail to start after changing features?
An optimized start needs the same --features list at build and run time. Keep the Dockerfile and cloudrun.tf values identical.
Does it support passkeys?
Yes, through Keycloak's WebAuthn passwordless policy. The README lists the realm settings.