When deploying full-stack web applications on Google Cloud Platform, Google Cloud Run is easily one of the most powerful and developer-friendly serverless container platforms available. You containerize your application (in my case, a Django + React trivia game called Poké Quiz), push it via GitHub Actions, and get autoscaling, health checks, and instant zero-to-one concurrency.
However, things got turbulent when connecting a custom subdomain (pokequiz.abelcreates.com).
Here is the story of how an innocent 😇 custom domain mapping spiral led to hours of SSL errors, why Cloud Run’s native domain mapping feature struggled, and how Firebase Hosting saved the architecture.
TL;DR
Use Firebase Hosting for custom domains, SSL termination, global CDN, and peace of mind. It’s free1 and it just works. 😅
The Symptom: The infinite SSL_ERROR_SYSCALL loop
After deploying Poké Quiz to Cloud Run in us-central1, I used Cloud Run’s native Custom Domain Mappings feature in the Google Cloud Console and created a standard CNAME record in DNS pointing to ghs.googlehosted.com.
Testing with curl:
$ curl -I https://pokequiz.abelcreates.com
curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to pokequiz.abelcreates.com:443
A verbose network inspection revealed a strange behavior:
- HTTP (Port 80) was answering with a
302 Foundredirecting tohttps://. - HTTPS (Port 443) immediately dropped the TCP connection during the TLS
Client Hellohandshake (read: errno=0, no peer certificate available).
In the GCP Console, the domain status stayed locked in a spinning state:
“Waiting for certificate provisioning. You must configure your DNS records for certificate issuance to begin.”
Even after deleting the mapping, re-adding it, and watching the certificate successfully provision for a few hours, the mapping abruptly broke again later that evening, dropping back to 404 Not Found on ghs.googlehosted.com and throwing SSL_ERROR_SYSCALL.
Why Cloud Run custom domain mappings failed
Behind the scenes, Cloud Run’s native custom domain mapping feature has been classified in “Preview” for years.
Here is why it frequently falls short in production:
- Aggressive Exponential Backoff on ACME Verification:
If you create the domain mapping in GCP before the DNS record has completely propagated worldwide, Google’s automated ACME bot encounters a verification failure on Attempt #1 and enters a multi-hour backoff sleep cycle. - DNSSEC & CNAME Resolution Latency:
With DNSSEC enabled, intermediate resolver caches often prevent Google’s backend verifier from resolving the ACME challenge cleanly. - Flapping Routing Tables on GHS:
ghs.googlehosted.com(Google Hosted Services) acts as a shared multi-tenant proxy. When background health checks or reconciliation routines hit transient timeouts, the domain mapping can become unbound, dropping your service back to a generic Google 404 page.
In fact, Google’s own official Cloud Run documentation now explicitly advises against using the legacy preview domain mapping for production, steering developers toward Google Cloud Load Balancing or Firebase Hosting.
The Alternative: Cloud Load Balancer vs. Firebase Hosting
Google provides two primary alternatives:
| Solution | Monthly Base Cost | Setup Complexity | CDN & Caching |
|---|---|---|---|
| Global External Application Load Balancer | ~$18 - $25/month (Forwarding rules + Serverless NEG) | High (Requires IP, SSL Policy, Backend Service, URL Maps) | Add-on (Cloud CDN) |
| Firebase Hosting (Reverse Proxy) | $0 / month (Free Tier) | Minimal (Single firebase.json configuration) |
Built-in Global Edge CDN |
For indie projects, showcase tools, and side applications, paying $20+/month just for a basic custom domain SSL termination is unnecessary.
Firebase Hosting is built directly on top of Google Cloud infrastructure, requires no separate billing, and natively supports Cloud Run Rewrites.
The Solution: Pairing Firebase Hosting with Cloud Run
Instead of fighting the preview domain mapping, Firebase Hosting acts as the Global Edge CDN and SSL Gateway, terminating HTTPS with a Let’s Encrypt / Google Trust Services certificate that auto-renews without downtime, and proxying 100% of the traffic directly to the Cloud Run container.
Step 1: Configure firebase.json
In the root of the project, we configured Firebase to route all wildcard traffic (**) directly to our Cloud Run service:
{
"hosting": {
"public": "frontend/dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"run": {
"serviceId": "pokefun",
"region": "us-central1"
}
}
]
}
}
Step 2: Configure Direct A Records
In my DNS (Squarespace), instead of a fragile CNAME chain, we pointed the subdomain directly to Firebase Hosting’s Anycast IP:
- Host:
pokequiz - Type:
A - Data:
199.36.158.100
Step 3: Automate in GitHub Actions CI/CD
To make the entire deployment continuous and hands-off, I updated .github/workflows/ci.yml so that every push to main builds the assets, deploys the container to Cloud Run, and refreshes Firebase Hosting:
- name: Build Frontend Assets
working-directory: ./frontend
run: |
npm ci
npm run build
- name: Deploy to Cloud Run
run: |
gcloud run deploy pokefun \
--source . \
--region us-central1
- name: Deploy to Firebase Hosting
run: |
npx -y firebase-tools deploy --only hosting --project pokequiz-505401
A quick note on security: You might notice the GCP project ID (
pokequiz-505401) is written explicitly in the step above. In Google Cloud and Firebase, project IDs are public identifiers rather than secrets. They are already exposed in default hosting domains (<project-id>.web.app), client-side configuration objects, and DNS records. Actual deployment permissions and resource access are strictly enforced via IAM roles, Workload Identity Federation, and authenticated credentials stored securely in GitHub Secrets.
The Result
Running curl -I https://pokequiz.abelcreates.com now returns:
HTTP/2 200
content-type: text/html; charset=utf-8
strict-transport-security: max-age=31556926
server: Google Frontend
x-served-by: cache-iad-kiad7000110-IAD
- ✅ Instant SSL handshake: Auto-managed TLS certificate via Google Trust Services with zero manual renewals.
- ✅ Global CDN caching: Assets are served from edge points of presence with low latency.
- ✅ Full Container Capability: Django REST Framework, WebSockets, background APIs, and SQLite/Postgres run seamlessly in Docker on Cloud Run.
- ✅ 100% Free & Scalable: Zero monthly overhead while remaining fully enterprise-scalable.
Key Takeaway
If you are running containerized apps on Google Cloud Run and need a custom domain, skip Cloud Run’s legacy Domain Mappings (Preview).
Using Firebase Hosting as a reverse proxy gives you the best of both worlds: a world-class global CDN with automated SSL at the edge, backed by the raw power of Cloud Run containers behind the scenes.
References
-
Firebase Hosting is a static site host, but it can also be used as a reverse proxy for dynamic Python applications. For projects with low traffic, this is a great free option 👍🏾. If you need more features, you can use Google Cloud Load Balancing. Once you go beyond a low-traffic state you’ll be reminded that Alphabet is a multi-trillion dollar company and they want their money 🤑. ↩