Configure GCP Firebase Hosting Custom Domains

The gcp:firebase/hostingCustomDomain:HostingCustomDomain resource, part of the Pulumi GCP provider, links custom domain names to Firebase Hosting sites, enabling Hosting to serve content on those domains with SSL certificates. This guide focuses on four capabilities: basic domain association, SSL certificate type selection, domain redirects, and Cloud Run service integration.

Custom domains require existing Firebase Hosting sites and DNS control for the domain names being configured. The examples are intentionally small. Combine them with your own DNS management and Firebase project configuration.

Most deployments start by connecting a domain name to an existing Hosting site, establishing the association that allows Firebase to serve content.

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

const _default = new gcp.firebase.HostingCustomDomain("default", {
    project: "my-project-name",
    siteId: "site-id",
    customDomain: "custom.domain.com",
});
import pulumi
import pulumi_gcp as gcp

default = gcp.firebase.HostingCustomDomain("default",
    project="my-project-name",
    site_id="site-id",
    custom_domain="custom.domain.com")
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/firebase"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := firebase.NewHostingCustomDomain(ctx, "default", &firebase.HostingCustomDomainArgs{
			Project:      pulumi.String("my-project-name"),
			SiteId:       pulumi.String("site-id"),
			CustomDomain: pulumi.String("custom.domain.com"),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var @default = new Gcp.Firebase.HostingCustomDomain("default", new()
    {
        Project = "my-project-name",
        SiteId = "site-id",
        CustomDomain = "custom.domain.com",
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.firebase.HostingCustomDomain;
import com.pulumi.gcp.firebase.HostingCustomDomainArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var default_ = new HostingCustomDomain("default", HostingCustomDomainArgs.builder()
            .project("my-project-name")
            .siteId("site-id")
            .customDomain("custom.domain.com")
            .build());

    }
}
resources:
  default:
    type: gcp:firebase:HostingCustomDomain
    properties:
      project: my-project-name
      siteId: site-id
      customDomain: custom.domain.com

The customDomain property specifies the domain name you control. The siteId references an existing Firebase Hosting site. After creation, Firebase provides DNS records (available in the requiredDnsUpdates output) that you must add to your domain’s DNS configuration to complete verification and enable HTTPS.

Configure certificate type and domain redirects

Some deployments need control over SSL certificate provisioning or want to redirect one domain to another for consolidation.

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

const _default = new gcp.firebase.HostingSite("default", {
    project: "my-project-name",
    siteId: "site-id-full",
});
const defaultHostingCustomDomain = new gcp.firebase.HostingCustomDomain("default", {
    project: "my-project-name",
    siteId: _default.siteId,
    customDomain: "source.domain.com",
    certPreference: "GROUPED",
    redirectTarget: "destination.domain.com",
    waitDnsVerification: false,
});
import pulumi
import pulumi_gcp as gcp

default = gcp.firebase.HostingSite("default",
    project="my-project-name",
    site_id="site-id-full")
default_hosting_custom_domain = gcp.firebase.HostingCustomDomain("default",
    project="my-project-name",
    site_id=default.site_id,
    custom_domain="source.domain.com",
    cert_preference="GROUPED",
    redirect_target="destination.domain.com",
    wait_dns_verification=False)
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/firebase"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_default, err := firebase.NewHostingSite(ctx, "default", &firebase.HostingSiteArgs{
			Project: pulumi.String("my-project-name"),
			SiteId:  pulumi.String("site-id-full"),
		})
		if err != nil {
			return err
		}
		_, err = firebase.NewHostingCustomDomain(ctx, "default", &firebase.HostingCustomDomainArgs{
			Project:             pulumi.String("my-project-name"),
			SiteId:              _default.SiteId,
			CustomDomain:        pulumi.String("source.domain.com"),
			CertPreference:      pulumi.String("GROUPED"),
			RedirectTarget:      pulumi.String("destination.domain.com"),
			WaitDnsVerification: pulumi.Bool(false),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var @default = new Gcp.Firebase.HostingSite("default", new()
    {
        Project = "my-project-name",
        SiteId = "site-id-full",
    });

    var defaultHostingCustomDomain = new Gcp.Firebase.HostingCustomDomain("default", new()
    {
        Project = "my-project-name",
        SiteId = @default.SiteId,
        CustomDomain = "source.domain.com",
        CertPreference = "GROUPED",
        RedirectTarget = "destination.domain.com",
        WaitDnsVerification = false,
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.firebase.HostingSite;
import com.pulumi.gcp.firebase.HostingSiteArgs;
import com.pulumi.gcp.firebase.HostingCustomDomain;
import com.pulumi.gcp.firebase.HostingCustomDomainArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var default_ = new HostingSite("default", HostingSiteArgs.builder()
            .project("my-project-name")
            .siteId("site-id-full")
            .build());

        var defaultHostingCustomDomain = new HostingCustomDomain("defaultHostingCustomDomain", HostingCustomDomainArgs.builder()
            .project("my-project-name")
            .siteId(default_.siteId())
            .customDomain("source.domain.com")
            .certPreference("GROUPED")
            .redirectTarget("destination.domain.com")
            .waitDnsVerification(false)
            .build());

    }
}
resources:
  default:
    type: gcp:firebase:HostingSite
    properties:
      project: my-project-name
      siteId: site-id-full
  defaultHostingCustomDomain:
    type: gcp:firebase:HostingCustomDomain
    name: default
    properties:
      project: my-project-name
      siteId: ${default.siteId}
      customDomain: source.domain.com
      certPreference: GROUPED
      redirectTarget: destination.domain.com
      waitDnsVerification: false

The certPreference property controls how Firebase provisions SSL certificates. GROUPED shares certificates across multiple domains (available on all plans), while PROJECT_GROUPED and DEDICATED provide isolated certificates (Blaze plan only). The redirectTarget property configures HTTP 301 redirects, sending all traffic from the custom domain to the specified destination. Setting waitDnsVerification to false allows the resource to complete before DNS verification finishes.

Route custom domain traffic to Cloud Run services

Applications that combine Firebase Hosting with Cloud Run can serve dynamic content through custom domains, with Hosting acting as the CDN and routing layer.

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

const _default = new gcp.firebase.HostingSite("default", {
    project: "my-project-name",
    siteId: "site-id",
});
const defaultService = new gcp.cloudrunv2.Service("default", {
    project: "my-project-name",
    name: "cloud-run-service-via-hosting",
    location: "us-central1",
    ingress: "INGRESS_TRAFFIC_ALL",
    template: {
        containers: [{
            image: "us-docker.pkg.dev/cloudrun/container/hello",
        }],
    },
    deletionProtection: true,
});
const defaultHostingVersion = new gcp.firebase.HostingVersion("default", {
    siteId: _default.siteId,
    config: {
        rewrites: [{
            glob: "/hello/**",
            run: {
                serviceId: defaultService.name,
                region: defaultService.location,
            },
        }],
    },
});
const defaultHostingRelease = new gcp.firebase.HostingRelease("default", {
    siteId: _default.siteId,
    versionName: defaultHostingVersion.name,
    message: "Cloud Run Integration",
});
const defaultHostingCustomDomain = new gcp.firebase.HostingCustomDomain("default", {
    project: "my-project-name",
    siteId: _default.siteId,
    customDomain: "run.custom.domain.com",
    waitDnsVerification: false,
});
import pulumi
import pulumi_gcp as gcp

default = gcp.firebase.HostingSite("default",
    project="my-project-name",
    site_id="site-id")
default_service = gcp.cloudrunv2.Service("default",
    project="my-project-name",
    name="cloud-run-service-via-hosting",
    location="us-central1",
    ingress="INGRESS_TRAFFIC_ALL",
    template={
        "containers": [{
            "image": "us-docker.pkg.dev/cloudrun/container/hello",
        }],
    },
    deletion_protection=True)
default_hosting_version = gcp.firebase.HostingVersion("default",
    site_id=default.site_id,
    config={
        "rewrites": [{
            "glob": "/hello/**",
            "run": {
                "service_id": default_service.name,
                "region": default_service.location,
            },
        }],
    })
default_hosting_release = gcp.firebase.HostingRelease("default",
    site_id=default.site_id,
    version_name=default_hosting_version.name,
    message="Cloud Run Integration")
default_hosting_custom_domain = gcp.firebase.HostingCustomDomain("default",
    project="my-project-name",
    site_id=default.site_id,
    custom_domain="run.custom.domain.com",
    wait_dns_verification=False)
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/cloudrunv2"
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/firebase"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_default, err := firebase.NewHostingSite(ctx, "default", &firebase.HostingSiteArgs{
			Project: pulumi.String("my-project-name"),
			SiteId:  pulumi.String("site-id"),
		})
		if err != nil {
			return err
		}
		defaultService, err := cloudrunv2.NewService(ctx, "default", &cloudrunv2.ServiceArgs{
			Project:  pulumi.String("my-project-name"),
			Name:     pulumi.String("cloud-run-service-via-hosting"),
			Location: pulumi.String("us-central1"),
			Ingress:  pulumi.String("INGRESS_TRAFFIC_ALL"),
			Template: &cloudrunv2.ServiceTemplateArgs{
				Containers: cloudrunv2.ServiceTemplateContainerArray{
					&cloudrunv2.ServiceTemplateContainerArgs{
						Image: pulumi.String("us-docker.pkg.dev/cloudrun/container/hello"),
					},
				},
			},
			DeletionProtection: pulumi.Bool(true),
		})
		if err != nil {
			return err
		}
		defaultHostingVersion, err := firebase.NewHostingVersion(ctx, "default", &firebase.HostingVersionArgs{
			SiteId: _default.SiteId,
			Config: &firebase.HostingVersionConfigArgs{
				Rewrites: firebase.HostingVersionConfigRewriteArray{
					&firebase.HostingVersionConfigRewriteArgs{
						Glob: pulumi.String("/hello/**"),
						Run: &firebase.HostingVersionConfigRewriteRunArgs{
							ServiceId: defaultService.Name,
							Region:    defaultService.Location,
						},
					},
				},
			},
		})
		if err != nil {
			return err
		}
		_, err = firebase.NewHostingRelease(ctx, "default", &firebase.HostingReleaseArgs{
			SiteId:      _default.SiteId,
			VersionName: defaultHostingVersion.Name,
			Message:     pulumi.String("Cloud Run Integration"),
		})
		if err != nil {
			return err
		}
		_, err = firebase.NewHostingCustomDomain(ctx, "default", &firebase.HostingCustomDomainArgs{
			Project:             pulumi.String("my-project-name"),
			SiteId:              _default.SiteId,
			CustomDomain:        pulumi.String("run.custom.domain.com"),
			WaitDnsVerification: pulumi.Bool(false),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var @default = new Gcp.Firebase.HostingSite("default", new()
    {
        Project = "my-project-name",
        SiteId = "site-id",
    });

    var defaultService = new Gcp.CloudRunV2.Service("default", new()
    {
        Project = "my-project-name",
        Name = "cloud-run-service-via-hosting",
        Location = "us-central1",
        Ingress = "INGRESS_TRAFFIC_ALL",
        Template = new Gcp.CloudRunV2.Inputs.ServiceTemplateArgs
        {
            Containers = new[]
            {
                new Gcp.CloudRunV2.Inputs.ServiceTemplateContainerArgs
                {
                    Image = "us-docker.pkg.dev/cloudrun/container/hello",
                },
            },
        },
        DeletionProtection = true,
    });

    var defaultHostingVersion = new Gcp.Firebase.HostingVersion("default", new()
    {
        SiteId = @default.SiteId,
        Config = new Gcp.Firebase.Inputs.HostingVersionConfigArgs
        {
            Rewrites = new[]
            {
                new Gcp.Firebase.Inputs.HostingVersionConfigRewriteArgs
                {
                    Glob = "/hello/**",
                    Run = new Gcp.Firebase.Inputs.HostingVersionConfigRewriteRunArgs
                    {
                        ServiceId = defaultService.Name,
                        Region = defaultService.Location,
                    },
                },
            },
        },
    });

    var defaultHostingRelease = new Gcp.Firebase.HostingRelease("default", new()
    {
        SiteId = @default.SiteId,
        VersionName = defaultHostingVersion.Name,
        Message = "Cloud Run Integration",
    });

    var defaultHostingCustomDomain = new Gcp.Firebase.HostingCustomDomain("default", new()
    {
        Project = "my-project-name",
        SiteId = @default.SiteId,
        CustomDomain = "run.custom.domain.com",
        WaitDnsVerification = false,
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.firebase.HostingSite;
import com.pulumi.gcp.firebase.HostingSiteArgs;
import com.pulumi.gcp.cloudrunv2.Service;
import com.pulumi.gcp.cloudrunv2.ServiceArgs;
import com.pulumi.gcp.cloudrunv2.inputs.ServiceTemplateArgs;
import com.pulumi.gcp.firebase.HostingVersion;
import com.pulumi.gcp.firebase.HostingVersionArgs;
import com.pulumi.gcp.firebase.inputs.HostingVersionConfigArgs;
import com.pulumi.gcp.firebase.HostingRelease;
import com.pulumi.gcp.firebase.HostingReleaseArgs;
import com.pulumi.gcp.firebase.HostingCustomDomain;
import com.pulumi.gcp.firebase.HostingCustomDomainArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var default_ = new HostingSite("default", HostingSiteArgs.builder()
            .project("my-project-name")
            .siteId("site-id")
            .build());

        var defaultService = new Service("defaultService", ServiceArgs.builder()
            .project("my-project-name")
            .name("cloud-run-service-via-hosting")
            .location("us-central1")
            .ingress("INGRESS_TRAFFIC_ALL")
            .template(ServiceTemplateArgs.builder()
                .containers(ServiceTemplateContainerArgs.builder()
                    .image("us-docker.pkg.dev/cloudrun/container/hello")
                    .build())
                .build())
            .deletionProtection(true)
            .build());

        var defaultHostingVersion = new HostingVersion("defaultHostingVersion", HostingVersionArgs.builder()
            .siteId(default_.siteId())
            .config(HostingVersionConfigArgs.builder()
                .rewrites(HostingVersionConfigRewriteArgs.builder()
                    .glob("/hello/**")
                    .run(HostingVersionConfigRewriteRunArgs.builder()
                        .serviceId(defaultService.name())
                        .region(defaultService.location())
                        .build())
                    .build())
                .build())
            .build());

        var defaultHostingRelease = new HostingRelease("defaultHostingRelease", HostingReleaseArgs.builder()
            .siteId(default_.siteId())
            .versionName(defaultHostingVersion.name())
            .message("Cloud Run Integration")
            .build());

        var defaultHostingCustomDomain = new HostingCustomDomain("defaultHostingCustomDomain", HostingCustomDomainArgs.builder()
            .project("my-project-name")
            .siteId(default_.siteId())
            .customDomain("run.custom.domain.com")
            .waitDnsVerification(false)
            .build());

    }
}
resources:
  default:
    type: gcp:firebase:HostingSite
    properties:
      project: my-project-name
      siteId: site-id
  defaultService:
    type: gcp:cloudrunv2:Service
    name: default
    properties:
      project: my-project-name
      name: cloud-run-service-via-hosting
      location: us-central1
      ingress: INGRESS_TRAFFIC_ALL
      template:
        containers:
          - image: us-docker.pkg.dev/cloudrun/container/hello
      deletionProtection: true
  defaultHostingVersion:
    type: gcp:firebase:HostingVersion
    name: default
    properties:
      siteId: ${default.siteId}
      config:
        rewrites:
          - glob: /hello/**
            run:
              serviceId: ${defaultService.name}
              region: ${defaultService.location}
  defaultHostingRelease:
    type: gcp:firebase:HostingRelease
    name: default
    properties:
      siteId: ${default.siteId}
      versionName: ${defaultHostingVersion.name}
      message: Cloud Run Integration
  defaultHostingCustomDomain:
    type: gcp:firebase:HostingCustomDomain
    name: default
    properties:
      project: my-project-name
      siteId: ${default.siteId}
      customDomain: run.custom.domain.com
      waitDnsVerification: false

This configuration creates a complete deployment flow: a Cloud Run service, a Hosting version with rewrite rules that route paths to the service, a release that activates the version, and finally the custom domain association. The rewrites configuration in HostingVersion directs traffic matching /hello/** to the Cloud Run service. The custom domain then serves this integrated application on your domain name.

Beyond these examples

These snippets focus on specific custom domain features: domain association and SSL certificate types, domain redirects, and Cloud Run integration. They’re intentionally minimal rather than full DNS management solutions.

The examples assume pre-existing infrastructure such as Firebase Hosting sites (or create them inline), DNS control for custom domains, and a Firebase project with appropriate billing plan. They focus on configuring the custom domain rather than managing DNS records or troubleshooting verification.

To keep things focused, common custom domain patterns are omitted, including:

  • DNS verification workflow (requiredDnsUpdates output)
  • Ownership state management (TXT record configuration)
  • Certificate state monitoring (certs output)
  • Host state troubleshooting (hostState and issues outputs)

These omissions are intentional: the goal is to illustrate how each custom domain feature is wired, not provide drop-in DNS management modules. See the Firebase Hosting CustomDomain resource reference for all available configuration options.

Let's configure GCP Firebase Hosting Custom Domains

Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.

Try Pulumi Cloud for FREE

Frequently Asked Questions

Domain Ownership & DNS
What do the different ownership states mean?

Ownership states indicate DNS record configuration status:

  • OWNERSHIP_MISSING: No Hosting-related ownership records found
  • OWNERSHIP_UNREACHABLE: DNS queries for ownership records failed
  • OWNERSHIP_MISMATCH: Domain owned by another Firebase project
  • OWNERSHIP_CONFLICT: Conflicting TXT records from multiple projects
  • OWNERSHIP_PENDING: DNS configured correctly, ownership transfers within 24 hours
  • OWNERSHIP_ACTIVE: Project has permission to act on domain’s behalf
How do I fix ownership issues?
For OWNERSHIP_MISMATCH or OWNERSHIP_CONFLICT, remove conflicting TXT records from other projects and add project-specific records for your current Firebase project. For OWNERSHIP_UNREACHABLE, check the issues output field for specific DNS errors.
What DNS records do I need to configure?
Check the requiredDnsUpdates output property for the specific DNS records Hosting needs to serve secure content on your domain.
What do the different host states mean?

Host states indicate IP address configuration:

  • HOST_UNHOSTED: Domain has no associated IP addresses
  • HOST_UNREACHABLE: DNS queries for IP addresses failed (check issues field)
  • HOST_MISMATCH: IPs don’t ultimately resolve to Hosting
  • HOST_CONFLICT: IPs resolve to both Hosting and other services (remove conflicting A/AAAA records)
  • HOST_ACTIVE: All requests served by Hosting
SSL Certificates
What's the difference between certificate types?
Three types are available: GROUPED (shared certificate), PROJECT_GROUPED (project-level shared), and DEDICATED (dedicated certificate). Spark plan CustomDomains can only use GROUPED, while Blaze plan can select any option.
How do I check my SSL certificate status?
Check the certs output property. For new CustomDomains, this often represents Hosting’s intent to create a certificate rather than an actual cert. Check the state field within certs for current status.
Configuration & Limitations
What properties can't I change after creation?
The customDomain, siteId, project, and waitDnsVerification properties are immutable. You must destroy and recreate the resource to change these values.
How do I redirect one domain to another?
Set the redirectTarget property to the destination domain name. Hosting will respond with HTTP 301 redirects and route traffic to the specified target.
What happens when I delete a custom domain?
Deleted CustomDomains persist for approximately 30 days before Hosting removes them completely. During this period, the deleteTime field is populated.
Troubleshooting & Status
What does the reconciling field mean?
When reconciling is true, Hosting’s systems are attempting to make the CustomDomain’s state match your preferred state. This is most common when initially provisioning a CustomDomain or creating a new SSL certificate after updating certPreference.
Why is my custom domain stuck in OWNERSHIP_PENDING?
OWNERSHIP_PENDING means your DNS records are configured correctly. Hosting will transfer ownership within 24 hours. If it remains in this state longer, check for DNS propagation delays.
Integration & Advanced
Can I use custom domains with Cloud Run services?
Yes. Create a gcp.firebase.HostingVersion with rewrites pointing to your Cloud Run service (using serviceId and region), then create the CustomDomain resource pointing to your site.

Using a different cloud?

Explore networking guides for other cloud providers: