Deploy a Static Site to Firebase Hosting: Step-by-Step (2026)

Ship a static site (Astro, Vite, Hugo, plain HTML) to Firebase Hosting in about 10 minutes — free SSL, custom domain, cache headers, and the macOS port-5000 fix. Verified June 2026.

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-toolsfirebase loginfirebase init hostingfirebase 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 5050 or 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:

ResourceSpark (free)Blaze overage rate
Stored site data10 GB$0.026 / GB
Data transfer360 MB / day$0.15 / GB
Custom domainsIncluded, free SSLsame
Multiple sites per projectSupportedsame
Credit card requiredNoYes

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 newerfirebase-tools v13+ dropped support for Node 16. Run node -v and npm -v to confirm both print versions.
  • A build command and output directory you know cold, usually npm run build producing dist/.
  • Optional but recommended: a domain you control plus access to its DNS records.

Step by step

  1. 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 init shows an empty project list later.
  2. 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.
  3. Build locally. In your repo, run npm run build. Confirm the output folder exists and contains real HTML, usually dist/index.html for Astro/Vite or build/index.html for Create React App and a few others.
  4. Install and log in to the CLI. Run npm install -g firebase-tools, then firebase login. If you juggle multiple Google accounts, run firebase login:list and confirm the active one is the account that owns the project — deploying from the wrong account is a common, silent mistake.
  5. 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, or out). Choose “No” for automatic GitHub deploys unless you want CI wired up today.
  6. 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.
  7. Verify the generated files. .firebaserc should hold your Firebase project ID, and firebase.json should point hosting.public at the build folder you checked in step 3.
  8. Add production-safe Hosting settings in firebase.json (full config below): cleanUrls: true for extensionless URLs, no-cache on HTML so visitors get fresh pages after each deploy, and a one-year immutable cache on hashed assets.
  9. Preview with Firebase, not just your dev server. Run firebase serve --only hosting (or firebase emulators:start --only hosting). On macOS the default port 5000 collides with AirPlay Receiver — if you see Port 5000 is not open or a blank page, add --port 5050 or turn off AirPlay Receiver in System Settings > General > AirDrop & Handoff. Test /, an article page, /404, /sitemap.xml, and /robots.txt.
  10. Deploy. Run npm run build once more, then firebase deploy --only hosting. Open the printed https://PROJECT_ID.web.app URL (Firebase also serves the same content at PROJECT_ID.firebaseapp.com) and click through the pages you just tested.
  11. 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.
  12. 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 automatic http://https:// redirect.
  13. 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.app URL 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:list and 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 init before creating a project, then finding an empty project list.
  • Deploying from the wrong Google account and pushing production to a personal test project.
  • Setting public to 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.html and breaks multi-page SEO.
  • Forgetting no-cache on 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.app URL, 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 serve show a blank page or “port 5000 not open” on my Mac?: macOS AirPlay Receiver listens on port 5000. Run firebase 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 in firebase.jsonfirebase.json only controls how files are served.
  • What about preview branches?: Run firebase hosting:channel:deploy NAME for a temporary preview URL that auto-expires (default 7 days, configurable with --expires).

Tags: #Indie dev #Firebase #Hosting #Getting started #Workflow