How to set up a custom domain and HTTPS redirects on Cloud Run

It's been a few years now, but I'm still excited by how Cloud Run can give you an HTTPS endpoint on the *.run.app domain in just a few steps. When you want to use a custom domain instead of the default URL, you have multiple options, including Firebase Hosting, Cloud Load Balancing, and a third party CDN. There's also the Custom Domain feature in Cloud Run, but the documentation says it's a preview feature and not recommended for production right now.

I'll show you how to set up a Cloud Run service with a custom domain through Cloud Load Balancing.

Through Cloud Load Balancing, you can set up an external HTTP(S) application load balancer. It doesn't have a free tier, but it allows you to add Cloud CDN caching, add Google Cloud Armor, create Google-managed SSL certificates, and much more. Refer to the Application Load Balancer overview in the Google Cloud documentation to learn more.

Here's what the steps are. First, enable the required APIs, reserve a static IP and update your DNS records. Then, start the provisioning process of a managed SSL certificate. While that's running, deploy a Cloud Run service, and setup the load balancer in front of it, including injected HSTS headers and the HTTP-to-HTTPS redirect.

Prerequisites

Before you start, make sure you have:

  • A domain name you control.
  • The gcloud CLI installed and authenticated.
  • A Google Cloud project with an attached billing account.

1. Enable the required APIs

Enable the Compute Engine (for the load balancer) and Cloud Run APIs in your Google Cloud project.

gcloud services enable compute.googleapis.com run.googleapis.com

2. Reserve a static IP and update DNS

The next step is to reserve a global static IP address and point your domain to it.

Reserve a static IP address

gcloud compute addresses create lb-ip --global

To see your IP address, run:

gcloud compute addresses describe lb-ip --global --format='get(address)'

Update your DNS

Go to your DNS provider and point your domain's A record to the static IP address you just reserved. By pointing your domain to the load balancer's IP, you give Google the proof it needs to issue a certificate for that domain. It's how you prove you own the domain.

Note that Google's validation service must be able to reach your load balancer's IP address directly. Intermediate layers such as a third-party CDN can cause the certificate provisioning to fail.

If Load Balancer Authorization doesn't work for you, there's an alternative way to prove ownership that requires creating a CNAME record for validation but offers more flexibility for complex configurations.

3. Provision a managed SSL certificate

To secure your traffic with HTTPS, you need an SSL certificate. Google Cloud handles provisioning and renewal for you. Replace [DOMAIN_NAME] with your own domain in the command below.

gcloud compute ssl-certificates create lb-cert \
    --domains=[DOMAIN_NAME] \
    --global

The certificate stays in a PROVISIONING state until the load balancer is live (you'll create it soon) and DNS is propagated. It typically takes up to 60 minutes to become ACTIVE once the configuration is complete.

4. Deploy an example Cloud Run service

Now, deploy a web service. I'm using the official NGINX image as an example here. It's a web server, which works great to prove this configuration works. You can deploy your own app later.

gcloud run deploy run-service \
    --image=nginx \
    --region=europe-west1 \
    --allow-unauthenticated \
    --port=80

The --allow-unauthenticated flag makes the service publicly accessible. The --port=80 flag is required because the NGINX image listens on port 80, whereas Cloud Run defaults to port 8080.

5. Configure a serverless backend

A serverless backend is a global resource and connects Cloud Load Balancing to your Cloud Run service.

Set up the backend service

The backend service acts as a central configuration point where you enable features like Cloud CDN caching, Cloud Armor, and custom headers.

gcloud compute backend-services create backend-service \
    --load-balancing-scheme=EXTERNAL_MANAGED \
    --custom-response-header='Strict-Transport-Security: max-age=31536000; includeSubDomains; preload' \
    --global

The --custom-response-header flag instructs the load balancer to automatically inject the Strict-Transport-Security HTTP header into all responses sent back to the client. This security header tells browsers to never use HTTP to make requests, preventing downgrade attacks.

Create a serverless network endpoint group

Now that you have created a backend service, you can connect the Cloud Run service to it. Cloud Load Balancing requires a regional configuration object for it, a serverless network endpoint group (NEG). This object represents the Cloud Run service within Cloud Load Balancing.

gcloud compute network-endpoint-groups create run-neg \
    --region=europe-west1 \
    --network-endpoint-type=serverless \
    --cloud-run-service=run-service

Add the serverless NEG to the backend service

Plug in the Cloud Run service to the backend using the NEG.

gcloud compute backend-services add-backend backend-service \
    --global \
    --network-endpoint-group=run-neg \
    --network-endpoint-group-region=europe-west1

Create the URL map

The URL map acts as the router for your load balancer. It inspects the hostname and path of incoming requests and directs them to the appropriate backend service. This configuration specifies to forward all requests to the backend service.

gcloud compute url-maps create url-map \
    --default-service=backend-service

6. Set up the HTTPS frontend

The frontend configuration defines how clients reach your service over HTTPS. It consists of a target proxy and a forwarding rule.

Create the target HTTPS proxy

The target HTTPS proxy terminates the SSL connection using the certificate you created in 3. Provision a managed SSL certificate.

gcloud compute target-https-proxies create https-proxy \
    --ssl-certificates=lb-cert \
    --url-map=url-map

Create the HTTPS forwarding rule

The forwarding rule connects your static IP address and port 443 (HTTPS) to the target proxy.

gcloud compute forwarding-rules create https-rule \
    --load-balancing-scheme=EXTERNAL_MANAGED \
    --network-tier=PREMIUM \
    --address=lb-ip \
    --target-https-proxy=https-proxy \
    --global \
    --ports=443

7. Set up the HTTP-to-HTTPS redirect

The HSTS header tells browsers to always use HTTPS. However, you still need a redirector to catch and upgrade a clients very first unencrypted request, in case they request the HTTP endpoint.

Create the redirect URL map

Use a special URL map that returns a 301 Moved Permanently redirect.

gcloud compute url-maps import redirect-map \
    --global \
    --source /dev/stdin <<EOF
name: redirect-map
defaultUrlRedirect:
  redirectResponseCode: MOVED_PERMANENTLY_DEFAULT
  httpsRedirect: True
EOF

Create the target HTTP proxy

gcloud compute target-http-proxies create http-proxy \
    --url-map=redirect-map

Create the HTTP forwarding rule

This rule uses the same static IP as the HTTPS rule, but listens on port 80.

gcloud compute forwarding-rules create http-rule \
    --load-balancing-scheme=EXTERNAL_MANAGED \
    --address=lb-ip \
    --target-http-proxy=http-proxy \
    --global \
    --ports=80

Verification

Once you have completed the configuration, you can verify that everything is working correctly.

Check certificate status

Google proves your domain ownership by attempting to reach your load balancer from multiple locations across the internet. You can monitor the progress of this automated proof using these commands:

# Check the overall certificate status (PROVISIONING -> ACTIVE)
gcloud compute ssl-certificates describe lb-cert \
    --global \
    --format="get(name,managed.status)"

# Check the specific domain validation status
gcloud compute ssl-certificates describe lb-cert \
    --global \
    --format="get(managed.domainStatus)"

If you see FAILED_NOT_VISIBLE, it usually means DNS has not yet propagated to all of Google's validation vantage points, or your load balancer isn't yet reachable on port 443.

Test HTTPS and redirects

Once the certificate is ACTIVE, test the endpoint using curl.

# Verify the HTTP to HTTPS redirect (should be 301)
curl -I http://[DOMAIN_NAME]

# Verify the HTTPS response and HSTS header
curl -I https://[DOMAIN_NAME]

In the output for the HTTPS request, you should see the strict-transport-security header and a 200 OK status.

Conclusion

Setting up a custom domain with an external load balancer takes a few more steps than the default *.run.app domain, and it's not part of the Google Cloud free tier. (Refer to the Cloud Load Balancing pricing page to learn more.) However, it gives you full control over your traffic and access to more advanced Google Cloud load balancing features.

Signature