You have a built static site sitting in dist/. You want it on a real HTTPS URL in about 10 minutes, with a custom domain, without reading the entire Firebase docs. This walkthrough does exactly that, names the two questions in firebase init that break sites when answered wrong, and includes the macOS quirk that makes firebase serve look broken when it isn’t.
TL;DR
- Firebase Hosting’s Spark (free) plan gives you 10 GB stored data, 360 MB/day of transfer, free auto-renewing SSL, a global CDN, and custom domains — all on a commercial site, no credit card. (firebase.google.com/pricing)
- Total command sequence:
npm install -g firebase-tools→firebase login→firebase init hosting→firebase deploy --only hosting. - The two answers that matter: set public directory to your build output (
dist, not the project root), and answer “single-page app” = No for any multi-page or Astro/Hugo site. - On macOS, the hosting emulator’s default port 5000 collides with AirPlay Receiver. Use
firebase serve --only hosting --port 5050or turn AirPlay Receiver off.
What Firebase Hosting gives you for free
Firebase Hosting is one of the fastest ways to put a static site on the open internet with auto-managed SSL and a CDN. Unlike Vercel’s Hobby tier (free but non-commercial only), the Firebase Spark plan permits commercial use, which matters if your site carries ads, affiliate links, or a product.
As of June 2026, here is what the free tier actually covers, with the upgrade thresholds:
| Resource | Spark (free) | Blaze overage rate |
|---|---|---|
| Stored site data | 10 GB | $0.026 / GB |
| Data transfer | 360 MB / day | $0.15 / GB |
| Custom domains | Included, free SSL | same |
| Multiple sites per project | Supported | same |
| Credit card required | No | Yes |
Source: Firebase pricing. A typical content or marketing site stays comfortably inside 360 MB/day; that ceiling is roughly 12,000 visits a day to a 30 KB HTML page before any CDN caching. If you outgrow it, the Blaze plan keeps the same free allotment and only bills the overage, so the move is a billing toggle, not a migration.
Before you start
- A Google account with access to the Firebase console.
- A static site repo that builds locally (Astro, Vite, Hugo, Eleventy, or plain HTML).
- Node.js 18 or newer —
firebase-toolsv13+ dropped support for Node 16. Runnode -vandnpm -vto confirm both print versions. - A build command and output directory you know cold, usually
npm run buildproducingdist/. - Optional but recommended: a domain you control plus access to its DNS records.
Step by step
- Create the Firebase project first. Open the Firebase console, click “Add project”, enter a name, keep or disable Google Analytics (you can skip it for a static site), and finish. Skipping this step is the #1 reason
firebase initshows an empty project list later. - Enable Hosting. In the new project, go to Build > Hosting and click “Get started”. You do not need to copy the quickstart commands it shows; clicking through just confirms Hosting is active for the project.
- Build locally. In your repo, run
npm run build. Confirm the output folder exists and contains real HTML, usuallydist/index.htmlfor Astro/Vite orbuild/index.htmlfor Create React App and a few others. - Install and log in to the CLI. Run
npm install -g firebase-tools, thenfirebase login. If you juggle multiple Google accounts, runfirebase login:listand confirm the active one is the account that owns the project — deploying from the wrong account is a common, silent mistake. - Link the repo to the project. Run
firebase init hosting. Choose “Use an existing project”, select the project you just made, and set the public directory to your build output (dist,build, orout). Choose “No” for automatic GitHub deploys unless you want CI wired up today. - Answer the single-page-app question carefully. Choose “No” for Astro, Hugo, Eleventy, and any multi-page static site. Choose “Yes” only for a client-side router app (React Router, Vue Router) where every route must serve
/index.html. Answering “Yes” by accident rewrites every URL to the homepage and silently destroys multi-page SEO. - Verify the generated files.
.firebasercshould hold your Firebase project ID, andfirebase.jsonshould pointhosting.publicat the build folder you checked in step 3. - Add production-safe Hosting settings in
firebase.json(full config below):cleanUrls: truefor extensionless URLs,no-cacheon HTML so visitors get fresh pages after each deploy, and a one-year immutable cache on hashed assets. - Preview with Firebase, not just your dev server. Run
firebase serve --only hosting(orfirebase emulators:start --only hosting). On macOS the default port 5000 collides with AirPlay Receiver — if you seePort 5000 is not openor a blank page, add--port 5050or turn off AirPlay Receiver in System Settings > General > AirDrop & Handoff. Test/, an article page,/404,/sitemap.xml, and/robots.txt. - Deploy. Run
npm run buildonce more, thenfirebase deploy --only hosting. Open the printedhttps://PROJECT_ID.web.appURL (Firebase also serves the same content atPROJECT_ID.firebaseapp.com) and click through the pages you just tested. - Add your custom domain. In Firebase console > Hosting > Add custom domain, add the TXT verification record first, then the A or CNAME records Firebase gives you.
- Wait for DNS and SSL. Most domains connect within minutes; give it a few hours before touching unrelated DNS. Test apex and
www, plus the automatichttp://→https://redirect. - Finish the launch. Update your canonical URLs and sitemap base URL, submit the sitemap in Google Search Console, and save a note with the project ID, deploy command, public directory, and DNS records.
Reference firebase.json
A production-safe config for an Astro / Vite / Hugo static site:
{
"hosting": {
"public": "dist",
"cleanUrls": true,
"trailingSlash": false,
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"headers": [
{
"source": "**/*.@(js|css|woff2|svg|webp|png|jpg)",
"headers": [{ "key": "Cache-Control", "value": "public,max-age=31536000,immutable" }]
},
{
"source": "**/*.html",
"headers": [{ "key": "Cache-Control", "value": "no-cache" }]
}
]
}
}
Why these two cache rules matter: hashed assets (app.4f1c.js) never change content under the same name, so a one-year immutable cache is safe and fast. HTML files keep the same name across deploys, so no-cache forces the browser to revalidate and pick up your new build instead of serving a stale page.
Commands you will run repeatedly:
npm run build # produce dist/
firebase serve --only hosting --port 5050 # local preview (5050 avoids macOS port 5000)
firebase deploy --only hosting # push live
firebase hosting:channel:deploy preview --expires 7d # share an expiring preview link
firebase hosting:sites:list # confirm which site got the deploy
After-launch verification
- Open the
*.web.appURL and your custom domain in an incognito window. Test homepage, a deep article URL, 404, sitemap, robots, and the mobile layout. - Run
firebase hosting:sites:listand confirm you deployed to the intended project — critical if you manage several Firebase projects. - In Firebase console, open Hosting > Release history and confirm the latest release time matches your deploy.
- After DNS settles, use Search Console URL Inspection to confirm Google can fetch the page and sees the canonical URL you expect.
Common pitfalls
- Running
firebase initbefore creating a project, then finding an empty project list. - Deploying from the wrong Google account and pushing production to a personal test project.
- Setting
publicto the source folder instead of the build output — the site goes live but blank. - Answering “single-page app = Yes” by accident, which rewrites every URL to
/index.htmland breaks multi-page SEO. - Forgetting
no-cacheon HTML, so visitors keep seeing the old page after each deploy. - Hitting the macOS port-5000 AirPlay collision and assuming the emulator is broken.
- Deploying from a dirty working tree without checking what files Firebase will upload.
Who this is for, and who should skip it
This is for anyone shipping a static site (Astro, Vite, Hugo, plain HTML) who wants HTTPS, a CDN, and a custom domain on a free, commercial-friendly tier.
Skip Firebase Hosting if your app needs server-side rendering on every request, real-time websockets, or region-pinned routing. Those belong on a framework-native host or on Firebase App Hosting (a separate product the CLI will suggest if it detects Next.js or Angular SSR during firebase init).
FAQ
- Do I need to pay anything to deploy?: No. The Spark (free) plan covers the deploy, the
*.web.appURL, custom domain, and SSL, with no credit card and no non-commercial restriction. Free quota is 10 GB stored data and 360 MB/day of transfer. - How long until my custom domain works?: Usually 15 minutes to a few hours after the DNS records are set. SSL provisioning runs in parallel and renews automatically.
- Can I roll back a bad deploy?: Yes. In Firebase console, Hosting > Release history > pick a previous release > Rollback. It is one click and instant.
- Why does
firebase serveshow a blank page or “port 5000 not open” on my Mac?: macOS AirPlay Receiver listens on port 5000. Runfirebase serve --only hosting --port 5050, or disable AirPlay Receiver under System Settings > General > AirDrop & Handoff. - Where do I put environment variables?: Static sites bake env vars at build time. Set them in your build pipeline (or
.env), not infirebase.json—firebase.jsononly controls how files are served. - What about preview branches?: Run
firebase hosting:channel:deploy NAMEfor a temporary preview URL that auto-expires (default 7 days, configurable with--expires).
Related
- What is Firebase Hosting
- Connecting a custom domain to Firebase Hosting
- Firebase Hosting go-live checklist
- Firebase Hosting Headers Config: Cache, Security, CORS
Tags: #Indie dev #Firebase #Hosting #Getting started #Workflow