all systems operationalIndia

Deploy a static site from GitHub to Cloudflare, free

Connect a GitHub repo to Cloudflare, build it on every push, and serve it from the edge on your own domain. No server, no bill, free HTTPS.

A portfolio, a blog or a docs site doesn't need a server. Every page can be generated once at build time and served as plain files. Running a VPS or a home machine just to hand out HTML means paying for uptime, patches and certificates you don't need.

Cloudflare can build the site straight from your GitHub repo and serve the result from its edge network. You push to main, Cloudflare runs your build, and a minute later the new version is live on your domain.

git push main ──▶ Cloudflare build: npm ci ──▶ npm run build (static files in out/) ──▶ npx wrangler deploy
                                                                                            │
visitor ──https──▶ example.com ──▶ Cloudflare edge ──▶ your static files, served from the nearest location

What you get:

  • No server to run. Nothing to patch, nothing to restart, nothing that goes down when your machine does.
  • Deploys on every push. Other branches get their own preview URLs.
  • A global CDN and free HTTPS. Static asset requests are free and unlimited on the free plan.
  • One-click rollbacks. Every deploy is kept, and any of them can be put back live.

This guide goes from "I have a site in a GitHub repo" to "it's live on my domain and redeploys itself". The examples use Next.js. Any framework that can output a folder of static files works the same way.

What you need

  • A site in a GitHub repository that can be built into static files (Next.js, Astro, Hugo, Vite, plain HTML, …).
  • A free Cloudflare account.
  • Optionally, a domain whose DNS is already on Cloudflare. If it isn't yet, Step 1 of the Cloudflare Tunnel guide covers moving the nameservers.
  • Node.js on your own machine, to test the build before Cloudflare does.

Throughout this post, replace example.com with your domain and my-site with your project's name.

Step 1: Make the site static

Cloudflare will serve whatever ends up in your build folder, so the build has to produce plain files. For Next.js, that means the static export.

One line in next.config.ts is enough:

import type { NextConfig } from "next";

const nextConfig: NextConfig = { output: "export" };

export default nextConfig;

A static export writes the site to out/. Two rules to know:

  • Nothing can run per request. Route handlers, cookies(), headers(), server actions and redirects() in next.config.ts don't work. Every dynamic route needs generateStaticParams, so the build knows which pages to create.
  • next/image can't optimise images. Either set images: { unoptimized: true } or use plain <img> tags.

If the same repo must also build as a normal server somewhere else, turn the export on only when WORKERS_CI (Workers) or CF_PAGES (Pages) is set. Cloudflare's builds set these automatically.

Other frameworks need nothing special. Just note their output folder: dist/ for Vite and Astro, public/ for Hugo.

Step 2: Tell Cloudflare what to serve

New projects created from a repo are Workers that serve static assets. A small file at the repo root, wrangler.jsonc, says which folder to upload and how to behave:

{
  "name": "my-site",
  "compatibility_date": "2026-04-01",
  "assets": {
    "directory": "./out",
    "not_found_handling": "404-page",
    "html_handling": "auto-trailing-slash"
  }
}
  • name must match the project name you pick in the dashboard. Otherwise the deploy creates a second project.
  • directory is your build output folder.
  • not_found_handling: "404-page" serves your 404.html with a real 404 status for unknown paths.
  • html_handling lets /about serve about.html without the extension showing in the URL.

Two more small files are worth adding:

  • public/_redirects for old URLs. The framework copies it into out/, and Cloudflare reads it there:

    /old-page        /new-page        301
    /posts/:slug     /blog/:slug      301
    
  • .node-version containing your local major version, for example 22 or 24. Without it, Cloudflare builds with its own default (Node 24 for Workers Builds), which may not match what you develop with.

Step 3: Test the build locally

Catch problems before Cloudflare does. Build the export, then serve it with Wrangler, which uses the same runtime as Cloudflare's edge:

npm run build
ls out/                  # index.html, 404.html, _redirects, your pages ...
npx wrangler dev         # serves out/ on http://localhost:8787

Then check the things the static setup is responsible for:

curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8787/            # 200
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8787/does-not-exist   # 404
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://localhost:8787/old-page   # 301 .../new-page

Commit wrangler.jsonc, public/_redirects and .node-version, and push. Add .wrangler/ to .gitignore, because wrangler dev keeps local state there.

Step 4: Connect the GitHub repo

  1. In the Cloudflare dashboard, go to Workers & Pages > Create application, and click Get started next to Import a repository.

  2. Connect GitHub. When GitHub asks which repositories the Cloudflare app may access, choose Only select repositories and pick this one. It doesn't need the rest of your account.

  3. Select the repo and fill in the settings:

    SettingValue
    Project namemy-site (the same as name in wrangler.jsonc)
    Production branchmain
    Build commandnpm run build
    Deploy commandnpx wrangler deploy
    Environment variablesnone needed
  4. Click Save and Deploy and open the build log. A healthy log shows npm ci, then your build exporting the pages, then Wrangler uploading the assets. When it finishes, the site is live at https://my-site.<account>.workers.dev.

From now on, every push to main triggers a new build and deploy.

Step 5: Put it on your own domain

A project's custom domain gets its own DNS record, of type Worker.

  1. DNS > Records: if example.com or www already points somewhere, delete those A, AAAA or CNAME records, or the next step fails. Leave email records (MX, and the TXT records for SPF, DKIM and DMARC) alone.
  2. Workers & Pages > my-site > Settings > Domains & Routes > Add > Custom domain: add example.com, then add www.example.com. Cloudflare creates the DNS records and issues the certificates itself, usually within a minute or two.
  3. Send www to the bare domain. Go to Rules > Templates, pick Redirect from WWW to root, turn on Preserve query string, and deploy. The dashboard warns that www may not be proxied. You can ignore that, because the Worker record for www is proxied. Choose Ignore and deploy rule anyway.

Check it:

curl -sI https://example.com | grep -iE '^(HTTP|server)'
# HTTP/2 200
# server: cloudflare
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' 'https://www.example.com/blog?x=1'
# 301 https://example.com/blog?x=1

Step 6: Living with it

  • Publishing: push to main. The build takes a minute or two.
  • Previews: pushes to any other branch build a preview version with its own URL, which is handy for checking a post before it goes live. To keep previews private, put them behind Zero Trust > Access with your email as the only allowed identity.
  • Rollbacks: open Deployments, find the last good version and promote it. This takes seconds and needs no new build.
  • Skipping a build: put [skip ci] in a commit message, for example for a README-only change.
  • Turn off workers.dev once your domain works, under Settings > Domains & Routes, so visitors and search engines stay on your domain. Preview URLs are separate and keep working.
  • Security headers: add a public/_headers file. Cloudflare reads it the same way as _redirects.

What about Cloudflare Pages?

Pages is Cloudflare's older product for exactly this job, and it still works. Cloudflare now points new projects at Workers, which can serve static assets as well and add server code later in the same project. The setup differs in only a few places:

Workers (this guide)Pages
WhereWorkers & Pages > Create application > Import a repositoryWorkers & Pages > Create application > Pages tab
What to serveassets.directory in wrangler.jsonc"Build output directory" setting in the dashboard
Build detection variableWORKERS_CICF_PAGES
Deploy commandnpx wrangler deploynone, Pages uploads the folder itself
_redirects and _headersSupportedSupported
Adding server code laterSame projectPages Functions

With a plain output: "export", the same repo builds on either one.

Troubleshooting

SymptomLikely causeFix
next/font/google fails during the buildIt downloads fonts at build time, which can fail in CIDownload the font files into the repo and use next/font/local
The log shows a different Node version from yoursNo .node-version fileAdd one with your major version
The deploy step tries to set up OpenNext and failswrangler.jsonc is missing, so Wrangler tries to auto-configure the project as a full Next.js appAdd wrangler.jsonc with an assets block
The build passes but there's no out/ folderThe static export isn't onCheck output: "export" in next.config.ts
Old links return 404_redirects isn't in the output folderPut it in public/
Adding the custom domain says a record already existsAn old DNS record uses that nameDelete it, then add the domain again
wrangler dev says the compatibility date is too newThe date is later than your Wrangler version supportsLower compatibility_date or update Wrangler

Recap

  1. Make the build produce static files (output: "export" for Next.js).
  2. Add wrangler.jsonc for the output folder and 404 page, _redirects for old URLs and .node-version.
  3. Test locally with npx wrangler dev.
  4. Import the repo in Workers & Pages, with npm run build and npx wrangler deploy.
  5. Add your custom domain and the www-to-root redirect.

Your site now deploys itself on every push, and serves from Cloudflare's edge with nothing for you to keep running.

Connected

shares a topic with this post