A domain, a site and an inbox, all on Cloudflare

Registering a domain, hosting a static site on Cloudflare Pages straight from a private GitHub repo, and forwarding email to it, plus the handful of gotchas that turned up the second time through.

October 4, 2026
cloudflare pagesgithubdnsemail routingastro

This site is the second time I’ve done the full setup: buy a domain, put a static site on it that deploys on git push, and give it a working email address. The second run went faster, but it hit almost all the same snags as the first. So here’s the whole recipe in one place.

Everything below runs on Cloudflare’s free tiers, except the domain itself.

Menu paths are a snapshot from October 2026. Cloudflare and GitHub rename and move things in their dashboards often, and without notice. If a menu path like Workers & Pages → Create no longer matches what you see, look for the feature by name: Pages, Custom domains, Email Routing, Installed GitHub Apps. The concepts and the DNS records stay the same even when the clicks change. The dig and curl checks at the end don’t depend on any dashboard.

1. Register the domain

Buy it through Cloudflare Registrar. It charges the wholesale price with no markup, and the domain lands in your Cloudflare account with DNS already managed there. You don’t have to change nameservers or wait for propagation.

  • .dev (and .app) are HTTPS-only. These TLDs are on the browser HSTS preload list, so plain http:// never works. Cloudflare provides the certificate, so this is no problem in practice. It just means you can’t do a quick plain-HTTP test.
  • Keep work and personal sites on separate domains. Moving a public hostname to another domain later is easy. Untangling which things share a domain is not.

2. Build the site

I use Astro because notes are just Markdown files in a folder, and the output is plain static HTML. Any static generator works, and so does a folder of hand-written HTML.

npm create astro@latest
npm run dev      # local preview with live reload
npm run build    # writes static files to dist/

3. Push to a private GitHub repo

Create an empty private repo on GitHub and push to it. Use the SSH remote. A machine with no HTTPS credential helper fails on push with could not read Username:

git remote add origin git@github.com:<you>/<repo>.git
git push -u origin main

4. Connect Cloudflare Pages

Go to Workers & Pages → Create → Pages → Import an existing Git repository, choose the repo, and set:

SettingValue
Framework presetAstro (or your generator)
Build commandnpm run build
Output directorydist

After that, every push to main builds and deploys in about a minute. Pushes to other branches get their own preview URLs.

Gotcha: Cloudflare can’t find the repo. This is a GitHub permission, not a Cloudflare problem. The first time you connect, Cloudflare’s GitHub app is usually installed with access to only selected repositories, so any repo you create afterwards is invisible to it. To fix it, go to GitHub → Settings → Applications → Installed GitHub Apps → Cloudflare Workers and Pages → Configure → Repository access, add the new repo, then refresh the Cloudflare page.

5. Attach the domain

Go to Pages project → Custom domains → Set up a custom domain. Because the domain’s DNS is already on Cloudflare, it creates the record and the certificate for you.

  • Add www. as a second custom domain too, so people who type it out of habit don’t hit an error. You don’t need a redirect if each page carries a canonical tag (<link rel="canonical" href="https://yourdomain/...">). Astro makes that a one-liner in the layout. It tells search engines which address is the real one.
  • Subdomains can live anywhere. The apex domain can be on Pages while other subdomains point to a Cloudflare Tunnel on a Raspberry Pi or an edge PC (see the remote access note). Each is just its own DNS record.

6. Email: forward, and lock down spoofing

Cloudflare shows a warning on any domain with no mail records: “Email cannot reach addresses and they could be spoofed.” Both halves of that matter, even if you never plan to use email on the domain.

To receive mail, go to Email → Email Routing → Get started:

  1. Create a custom address (for example contact@) and set its destination to an inbox you already use.
  2. Click the verification link Cloudflare emails to that inbox. Forwarding stays off until you do.
  3. Let it add its DNS records: three route*.mx.cloudflare.net MX records and an SPF record, v=spf1 include:_spf.mx.cloudflare.net ~all.
  4. Test from a different account. Mail you send from the same inbox it forwards to may not show up as new mail.

Then add DMARC yourself, because Email Routing doesn’t create it. Add a TXT record named _dmarc with the content v=DMARC1; p=reject;. If you only receive through Cloudflare and never send as the domain, p=reject is correct: anything claiming to be from your domain is fake.

Gotchas:

  • “Existing non-Cloudflare MX records conflict.” If you earlier added “no mail here” records (a null MX 0 . and SPF v=spf1 -all), delete both before enabling routing. A domain can only have one SPF record, and Cloudflare is about to add its own.
  • Only the addresses you create work. Mail to any other name bounces unless you turn on the catch-all, and the catch-all mostly brings in spam. Either create one address per purpose, or test whether plus-addressing (you+shop@) reaches you.
  • Email Routing only receives. Replies go out from your normal inbox’s address. Sending as the domain needs an SMTP service, and SPF and DMARC have to be updated to match.

If you don’t want email on the domain at all, skip routing and publish “no mail” records instead: a null MX 0 ., SPF v=spf1 -all, and DMARC v=DMARC1; p=reject;. Your sites aren’t affected either way, because these records only change how other mail servers treat mail claiming to be from your domain.

Checking it from a terminal

dig +short MX yourdomain @1.1.1.1
dig +short TXT yourdomain @1.1.1.1          # one SPF record only
dig +short TXT _dmarc.yourdomain @1.1.1.1
curl -sI https://yourdomain/ | head -1       # expect HTTP/2 200
curl -sI https://www.yourdomain/ | head -1

The result

The domain costs a fixed yearly price, and everything else here is free. Publishing a page is git push. The site doesn’t depend on any computer at home being switched on, and mail to the domain reaches an inbox I already check.

← all notes