Host a website for free with GitHub and Cloudflare, step by step
How to take a website from a folder of files to a live address with your own domain, HTTPS, free email forwarding and visitor statistics. Design, repository, Cloudflare deployment, domain, launch checklist and measuring, with the mistakes to avoid.
How does a folder of text files on your computer become a website anyone in the world can open? You save the files in a repository, a hosting service builds them whenever you push a change, and a domain name points visitors to the result. This guide walks through every step with free tools: GitHub for the files, Cloudflare for hosting, HTTPS, email forwarding and statistics, and one domain name that costs about the price of a lunch per year.
Edit a file → git push → GitHub → Cloudflare builds the site → yourdomain.comAn analogy. The domain is the street address, the repository is the building plans, and Cloudflare is the builder and delivery service. You change the plans, and the builder rebuilds the house and delivers it to the same address.
Everything here is a static site: pages that are generated once and then served as plain files. It is fast, safe, and almost free to run. Replace yourdomain.com and my-site with your own names throughout.
What it costs
| Item | Cost | Notes |
|---|---|---|
| GitHub repository | Free | Public or private both work |
| Cloudflare hosting | Free | Includes HTTPS and a free address ending in .pages.dev or .workers.dev |
| Domain name | About 10 to 12 USD per year | The only cost you cannot avoid |
| Email forwarding on your domain | Free | Cloudflare Email Routing |
| Visitor statistics | Free | Cloudflare analytics and Google Search Console |
Free plans have limits, such as a monthly number of builds and a maximum file size. They are generous for a personal site, but check the current numbers on Cloudflare’s pricing page before you plan something large.
1. Design it simple first
A good site is decided before any code. Spend ten minutes answering four questions.
- Who is it for? One visitor type is easier to serve than five.
- What should they do? Read, download, contact you. Every page should lead to one of these.
- Which pages do you need? Start small.
- What is the first screen? Your name or topic, one sentence, one button.
A small page plan
| Page | Purpose | Contains |
|---|---|---|
| Home | First impression | Who you are, one sentence, main button, three highlights |
| About | Trust | Short story, skills, a way to contact you |
| Work or Projects | Proof | Cards with a title, one line and a link |
| Blog | Search traffic | Tutorials that answer real questions |
| Contact | Action | Email address or a simple form |
Design rules that keep it professional
- One stylesheet, two or three colours, two fonts. Fewer choices look more confident.
- Readable text. Body text around 17 to 19 px, lines no wider than about 70 characters, strong contrast.
- Mobile first. More than half of visitors will arrive on a phone, so design the narrow screen first.
- No third-party scripts. Ads, trackers and external fonts slow the page and raise privacy questions. Host your own fonts and images.
- Light and dark theme if you can, using the visitor’s system setting.
- Descriptive addresses.
/blog/my-first-post/beats/post1.html, for people and for search engines. - Small images. Use WebP or compressed JPG, and short looping videos instead of heavy GIFs.
Keep content separate from design
The most useful decision is to store your content as plain files and let a small generator turn them into pages. Then you edit text, never HTML.
my-site/
├── build.py # the generator (or any static site tool)
├── requirements.txt # packages the generator needs
├── content/
│ ├── site.yml # name, links, home page text
│ └── blog/ # one Markdown file per post
├── templates/ # page layouts (header, footer, ...)
├── static/ # css, js, images, fonts, files to download
└── _site/ # generated output, never committedYou can use a ready-made tool instead of writing your own, such as Hugo, Jekyll, Eleventy or Astro. Cloudflare builds all of them. The steps below only assume that one command produces a folder of finished pages.
2. Prepare the repository
Create it
- Create an account on GitHub and make a new repository, for example
my-site. Private is fine, because Cloudflare reads it through an app you authorise. - Put your files in it and commit.
git init
git add -A
git commit -m "First version"
git branch -M main
git remote add origin https://github.com/YOUR-NAME/my-site.git
git push -u origin mainDo not commit the generated folder
Add the output folder to .gitignore, so the repository holds only the sources:
_site/
__pycache__/Test the build on your computer first
A build that works locally almost always works on Cloudflare.
pip install -r requirements.txt
python3 build.pyIf it prints an error, fix it now. Reading the same error in a remote build log is slower.
Two checks that save hours
| Check | Why |
|---|---|
| Use spaces, never tabs, in YAML files | One invisible tab character stops the whole build. The error says found character '\t' that cannot start any token. |
| Never store passwords or keys in the repository | Git remembers every old version. A secret you delete later is still in the history. Use environment variables or the dashboard’s secret settings. |
3. Deploy on Cloudflare for free
Create the project
- Create a free Cloudflare account.
- Open Workers & Pages and choose Create application.
- Choose to import an existing Git repository and authorise GitHub. Pick
my-site, and give access to that one repository only. - Name the project. Its free address will be
my-site.pages.devormy-site.workers.dev, depending on which product the dashboard sets up.
Cloudflare has two ways to host a static site: Pages, and Workers with static assets. Both are free for this use, and the dashboard may steer you to either one. The settings are almost identical. Where they differ, both are shown.
Fill in the build settings
| Setting | Value | Meaning |
|---|---|---|
| Production branch | Your main branch, for example main |
Pushes to this branch go live |
| Build command | python build.py |
The one command that creates your pages |
| Output directory (Pages) | _site |
The folder to publish |
| Deploy command (Workers) | npx wrangler deploy |
Publishes the output folder |
| Root directory | / |
Where your files start |
| Environment variables | None for a simple site | Add secrets here, never in the repository |
If your repository has a requirements.txt, Cloudflare installs those packages by itself before the build command runs. You do not need to put pip install in the build command.
Workers only: add one small config file
A Worker needs to know which folder to publish. Create a file named wrangler.jsonc in the root of the repository, next to build.py:
{
"name": "my-site",
"compatibility_date": "2026-10-01",
"assets": {
"directory": "./_site",
"not_found_handling": "404-page"
}
}namemust match the project name in the dashboard.directoryis your output folder.404-pageshows your own 404 page for addresses that do not exist.- Keep only one Wrangler config file. A second one, such as
wrangler.toml, can conflict.
Pages projects do not need this file.
Read the build log
Every push starts a build. Open Deployments and read the log. A healthy build moves through five stages.
| Stage | What happens | If it is missing or fails |
|---|---|---|
| Initializing | A clean build machine starts | Usually a temporary Cloudflare problem. Retry. |
| Cloning | Your repository is downloaded | Check the GitHub authorisation and the branch name |
| Installing | requirements.txt packages are installed |
Check the package names in requirements.txt |
| Building | Your build command runs | Read the error under it. This is where most problems are. |
| Deploying | The finished folder goes live | Check the output directory or wrangler.jsonc |
If the Building stage never appears, the build command is empty. If the build fails, the previous version stays online, so a mistake never takes your site down.
When all stages are green, open your free address. You now have a live site with HTTPS and no cost.
4. Add your own domain
Choose the name
- Prefer
.com. It is what people type by default and it looks the most trustworthy. - Use
.orgonly for a community or non-profit project. - Your own name often works better for search than a brand name. You can register both and redirect one to the other.
- Short, easy to spell, no hyphens.
Register it
The simplest route is Cloudflare Registrar, because everything is in one dashboard: Domain Registration > Register Domains, search for the name and check out. Cloudflare sells domains at its own cost without a markup, and the price includes privacy protection for your contact details.
If you buy from another registrar, add the domain to Cloudflare as a site and change the nameservers at the registrar to the two Cloudflare gives you. Propagation can take from a few minutes to a day.
Turn on auto-renew. An expired domain breaks every link you ever shared.
Connect it to the project
- Open your project, then Custom domains (Pages) or Domains & Routes (Workers).
- Add
yourdomain.com. - Add
www.yourdomain.comtoo. - Wait until both show Active. DNS records and the HTTPS certificate are created for you.
If the dashboard says the domain is already in use, it is attached to an older project or Worker. Remove it there first, then add it again.
Send www to the bare address
Two addresses for the same page split your search ranking. Pick one, usually the bare yourdomain.com, and redirect the other.
In the dashboard, open your domain, then Rules > Redirect Rules, and create:
| Field | Value |
|---|---|
| When the address matches | www.yourdomain.com/* |
| Redirect to | https://yourdomain.com/${1} |
| Status code | 301 (permanent) |
Tell the site its new address
Your generator should know the official address, because it writes canonical links, the sitemap and the feed from it. Change it once, in one place:
url: https://yourdomain.comThen search the project for any old copies of the free address, including in documents such as your CV or README:
grep -rn "my-site.pages.dev" . --exclude-dir=.git --exclude-dir=_siteReplace every match, commit and push. Wait for the build, then open the site and do a hard refresh (Ctrl+Shift+R) in case your browser kept the old version.
Check the security setting
In SSL/TLS, the encryption mode should be Full or Full (strict) for a site hosted on Cloudflare. A visitor should see the padlock on both yourdomain.com and www.yourdomain.com.
5. Optional: free email on your domain
A professional address such as contact@yourdomain.com costs nothing with Email Routing. It forwards mail to an inbox you already use. It is a forwarding service, not a separate mailbox.
- In the dashboard, open your domain, then Email > Email Routing, and enable it. Let Cloudflare add its mail records.
- Under Destination addresses, add the inbox you want to receive mail in, and click the verification link Cloudflare sends there.
- Under Routing rules, create a custom address. Type only the part before the
@, choose Send to an email, and pick the destination. The Save button stays grey until the pattern, the action and the destination are all filled in. - Test it from a different account, not from the destination inbox itself.
Keep the catch-all rule on Drop. Otherwise every guessed address collects spam.
Replying from the new address needs an extra step in your mail service, often called Send mail as, and a matching SPF record so the mail does not land in spam. Skip it at first. Receiving is enough for most sites.
6. Launch checklist
Before you tell anyone, walk through this list on a phone and on a computer.
| Check | Done |
|---|---|
Home page loads on https://yourdomain.com with a padlock |
☐ |
www.yourdomain.com redirects to the bare address |
☐ |
| Every menu link works, and the 404 page looks right | ☐ |
| Images, fonts and downloads load | ☐ |
| Each page has its own title and description | ☐ |
| The site looks right in dark mode and on a narrow screen | ☐ |
| The contact form or email address works | ☐ |
https://yourdomain.com/sitemap.xml opens |
☐ |
https://yourdomain.com/robots.txt opens |
☐ |
| No old free-address links remain | ☐ |
| Social links and your own profiles point to the new domain | ☐ |
Then update the website link on your social profiles, your email signature and any documents that carry it. One address everywhere builds trust and search ranking.
7. Your daily workflow
Once it is set up, publishing is three steps.
python3 build.py --serve # preview at http://localhost:8000
git add -A
git commit -m "Add a new post"
git push # Cloudflare builds and publishes- Preview first. Look at the change locally before you push.
- One change at a time. If a build fails, you know which change caused it.
- A failed build is harmless. The previous version stays online until a build succeeds.
- Go back in time. Every deployment is listed under Deployments, and older ones can be restored if you need to.
- Editing on GitHub works too. Open a file, click the pencil, and commit. It triggers the same build. Pull the change to your computer afterwards.
8. Measure your visitors
Cloudflare traffic analytics
Open your domain, then Analytics & Logs > Traffic, and pick a time range. You get requests, visitors, countries and popular pages. The numbers are counted on Cloudflare’s servers, so your pages need no extra script and the privacy of your visitors stays intact.
Hosting-level numbers are also available on the project itself, under Metrics.
Google Search Console
This shows how people find you on Google. It is the most useful free tool for a blog.
- Open Search Console and choose Add property > Domain.
- Enter
yourdomain.com. Google asks you to add a TXT record to your DNS. With Cloudflare as your DNS provider, this takes one click or one copy and paste. - After verification, open Sitemaps and submit
https://yourdomain.com/sitemap.xml. - Wait a few days. Then check Performance for searches, clicks and the pages that appear.
What each tool tells you
| Question | Tool |
|---|---|
| How many people visited this week? | Cloudflare Traffic |
| Which countries do they come from? | Cloudflare Traffic |
| Which searches bring them here? | Search Console |
| Which pages does Google show? | Search Console |
| Is the site up and error-free? | Project Metrics |
| How long do people stay, and what do they click? | Needs a page-level analytics script (see below) |
Read the numbers honestly
- Requests are not people. One page view triggers many requests for style sheets, fonts and images.
- Bots count too. Crawlers and scanners inflate visitor numbers, especially on a new site.
- Small numbers are normal at first. A blog grows slowly. Compare this month with last month, not with a famous site.
Optional: Cloudflare Web Analytics
For counts of real page views, you can enable Web Analytics. It uses no cookies, but it works by loading a small Cloudflare script into your pages. If your footer promises “no third-party requests”, change the wording when you turn it on, for example to “No cookies, no ad trackers.”
Common problems and fixes
| Problem | Fix |
|---|---|
| The build fails after 0 seconds with no message | Open the build page and read the full log, then retry. If the log is empty, push a small new commit. |
| The log goes from Installing straight to Deploying | The build command is empty. Set it to your generator’s command. |
found character '\t' that cannot start any token |
A tab in a YAML file. Retype the indentation and the gap after the colon with spaces. |
| Deploy fails, “nothing to deploy” or “directory not found” | The output folder was never created, or the name is wrong. Check the build command, the output directory and wrangler.jsonc. |
| The site shows the old design | The branch is wrong, or the build failed. Check the production branch and the latest build. Then hard refresh. |
| The domain stays pending | Wait a few minutes. Check that no older project holds the domain and that the DNS records exist. |
www does not redirect |
Create the Redirect Rule and make sure the www DNS record is proxied (orange cloud). |
| The padlock shows a warning | Wait for the certificate to be issued, and set SSL/TLS to Full. |
| Email Routing says “Syncing” for a long time | Wait up to 30 minutes, then use the button that adds or fixes the records. |
| The Save button for a routing rule is grey | Fill in the pattern, the action and a verified destination. |
| A form on the site stopped delivering after you changed its address | Form services usually ask you to confirm a new address once. Send a test and click the confirmation link. |
| A document in the repository still shows the old address | Search the whole repository for the old address, including documents and templates. |
Security and privacy habits
- Never commit passwords, API tokens or private keys. If you did, change them, because deleting the file does not remove them from the history.
- Do not build an admin panel with a password in the page. A static site needs none, because you edit files in Git.
- Give Cloudflare access to one repository, not your whole GitHub account.
- Turn on two-factor authentication on GitHub, Cloudflare and your email.
- Keep the registrar contact details private, and keep your recovery email current.
Further reading
- Cloudflare Pages documentation
- Workers static assets
- Cloudflare Email Routing
- Cloudflare Web Analytics
- Google Search Console
- GitHub Docs: getting started with Git
Try it yourself
- Make a repository with one
index.htmlthat says “Hello”, and publish it on the free Cloudflare address. - Break the build on purpose, for example with a typo in the build command, and read what the log says. Then fix it and watch the previous version stay online meanwhile.
- Register a cheap domain, connect it, and make
wwwredirect to the bare address. - Create
contact@yourdomain.com, forward it to your inbox, and send yourself a test. - Submit your sitemap to Search Console, and check back after one week to see your first search impressions.