The gcp:firebase/hostingCustomDomain:HostingCustomDomain resource, part of the Pulumi Google Cloud 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 linking, SSL certificate type selection, domain redirects, and Cloud Run service integration.
Custom domains require existing Firebase Hosting sites and DNS control for the domain. Cloud Run integration requires additional Hosting version and release resources. The examples are intentionally small. Combine them with your own DNS configuration and backend services.
Link a custom domain to a Hosting site
Most deployments start by connecting a domain name to an existing site, allowing Hosting to serve content on that domain.
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 want to use. The siteId references an existing Firebase Hosting site. After creating this resource, you’ll need to configure DNS records (provided in the requiredDnsUpdates output) to verify ownership and route traffic to Firebase Hosting.
Configure certificate type and domain redirects
Some deployments need to control SSL certificate grouping or redirect one domain to another, such as redirecting www to the apex domain.
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 (Spark plan only option), while PROJECT_GROUPED and DEDICATED provide more isolation (Blaze plan). The redirectTarget property makes Hosting respond with HTTP 301 redirects, routing traffic to the specified destination domain. Setting waitDnsVerification to false allows Terraform to proceed without waiting for DNS propagation, storing any issues in state for later resolution.
Route custom domain traffic to Cloud Run services
Applications that combine Firebase Hosting with Cloud Run can serve dynamic content from containerized services while using Hosting’s CDN and custom domain features.
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 integration chain: a Cloud Run service, a Hosting version with rewrite rules, a release to activate that version, and finally the custom domain. The rewrites block in HostingVersion routes requests matching /hello/** to the Cloud Run service. The custom domain then makes this entire setup accessible via your own domain name, with Firebase Hosting handling SSL and CDN.
Beyond these examples
These snippets focus on specific custom domain features: custom domain linking and DNS verification, SSL certificate management and domain redirects, and Cloud Run integration with rewrite rules. They’re intentionally minimal rather than full hosting deployments.
The examples may reference pre-existing infrastructure such as Firebase Hosting sites (or created inline), DNS control for custom domains, and Cloud Run services (for integration example). They focus on configuring the custom domain rather than provisioning everything around it.
To keep things focused, common custom domain patterns are omitted, including:
- DNS record configuration details (requiredDnsUpdates output)
- Ownership verification with TXT records
- Certificate state monitoring (certs, hostState, ownershipState outputs)
- Error handling for DNS issues (issues output)
These omissions are intentional: the goal is to illustrate how each custom domain feature is wired, not provide drop-in hosting 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 FREEFrequently Asked Questions
Setup & Configuration
customDomain, project, siteId, and waitDnsVerification properties are immutable and require resource replacement if changed.waitDnsVerification is true, Terraform waits for DNS records to be fully resolved before completing. If false, Terraform proceeds immediately and stores any DNS issues in state.SSL Certificates
Firebase Hosting offers three certificate types via certPreference:
- GROUPED: Shared certificate (only option for Spark plan)
- PROJECT_GROUPED: Project-level shared certificate (Blaze plan only)
- DEDICATED: Dedicated certificate for your domain (Blaze plan only)
DNS & Ownership Verification
Ownership is determined by TXT records in your DNS:
- OWNERSHIP_MISSING: No ownership records found
- OWNERSHIP_UNREACHABLE: DNS queries failed
- OWNERSHIP_MISMATCH: Owned by another Firebase project
- OWNERSHIP_CONFLICT: Conflicting TXT records from multiple projects
- OWNERSHIP_PENDING: DNS configured correctly, ownership transfer within 24 hours
- OWNERSHIP_ACTIVE: Your project has verified ownership
requiredDnsUpdates output property after creating the custom domain. It provides the specific DNS records needed for your domain to work with Hosting.Domain States & Troubleshooting
Host state indicates whether your domain’s IP addresses serve Hosting content:
- HOST_UNHOSTED: No IP addresses associated with domain
- HOST_UNREACHABLE: DNS queries failed (check
issuesfield) - HOST_MISMATCH: IP addresses don’t resolve to Hosting
- HOST_CONFLICT: IP addresses resolve to both Hosting and other services
- HOST_ACTIVE: All requests served by Hosting
reconciling is true, Hosting’s systems are actively working to match your custom domain’s actual state with your desired configuration. This commonly occurs during initial provisioning or when creating new SSL certificates.Redirects & Advanced Features
redirectTarget property to the destination domain. Hosting will respond with HTTP 301 redirects, routing traffic to the specified target instead of serving content directly.HostingVersion with rewrites pointing to your Cloud Run service (using serviceId and region), then associate your custom domain with the site.Lifecycle & Deletion
deleteTime value.Using a different cloud?
Explore networking guides for other cloud providers: