How I Deploy Free Websites with Cloudflare
I deploy free websites globally using Cloudflare Workers and git. You can too!
Here's my step-by-step for getting a free website deployed on Cloudflare, minus the cost of the domain.
- Buy a domain
- Set the name servers to Cloudflare
- Deploy the Wrangler Static Site template from CLI
- Make your static site changes and test locally
- Push to GitHub
- Setup Cloudflare Workers to pull from GitHub
- Set your custom domain to point to your worker
The conceit I've been dreaming about for this post is to see if I can deploy a static site to Cloudflare Workers for free, and post about the complete process, in less than an hour.
It's now 7:50PM as I start. I failed, because this took me 1h 45m in total. It's a lot longer to write text and get screenshots as you go.
Buy a Domain
I already own burns.sh and for this purpose I was going to use https://demo.burns.sh, which doesn't exist yet. I think I pay about $12 per year for this. Not totally free, but not expensive either.
If you buy the domain through Cloudflare, they handle all the nameserver and DNS config for you.
Set the Name Servers to Cloudflare
I purchase the domain through Porkbun, so I have to change the NS records to Cloudflare servers.
NS celine.ns.cloudflare.com
NS lou.ns.cloudflare.com
These records might be different for you, and the Cloudflare domain onboarding step will walk you through this. Punch those records into your NS config in your domain DNS configuration.
Deploy the Wrangler Static Site Template from CLI
In your favorite terminal or IDE, you'll need to have some prerequisites setup for this.
https://developers.cloudflare.com/workers/wrangler/install-and-update/
Install Node.js and nvm. For me this is a simple apt install away.
Create your new blank directory. For me this is
mkdir ~/devel/demo-burns
cd ~/devel/demo-burns
Install Wrangler
npm i -D wrangler@latest
You can check that this is working
npx wrangler --version
⛅️ wrangler 4.70.0
───────────────────
Your directory should now have some more files in there.
ls -l
total 56
drwxrwxr-x 30 burns burns 4096 Mar 3 20:02 node_modules
-rw-rw-r-- 1 burns burns 57 Mar 3 20:02 package.json
-rw-rw-r-- 1 burns burns 48336 Mar 3 20:02 package-lock.json
If this is your first time, walk through the npx wrangler login process.
Make sure that worked with
npx wrangler whoami
👋 You are logged in with an OAuth Token, associated with the email <redacted>
Next up, let's init this project, setup the initial config files, and use the static site template.
npx wrangler init
╭ Create an application with Cloudflare Step 1 of 3
│
╰ In which directory do you want to create your application? also used as application name
│ dir ./demo-burns
│
╰ What would you like to start with?
● Hello World example
│
╰ Which template would you like to use?
○ Worker only
● Static site
├ Copying template files
│ files copied to project directory
│
├ Updating name in `package.json`
│ updated `package.json`
│
╰ Do you want to add an AGENTS.md file to help AI coding tools understand Cloudflare APIs?
Yes / No
╰ Application created
╭ Configuring your application for Cloudflare Step 2 of 3
│
├ Retrieving current workerd compatibility date
│ compatibility date 2026-03-01
│
╰ Do you want to use git for version control?
Yes / No # I selected yes here. This is critical for later.
│ yes git
│
├ Initializing git repo
│ initialized git
│
├ Committing new files
│ git commit
│
╰ Application configured
╭ Deploy with Cloudflare Step 3 of 3
│
╰ Do you want to deploy your application?
Yes / No # I selected no here. We're not ready quite yet.
├ Do you want to deploy your application?
│ no deploy via `npm run deploy`
│
╰ Done
🎉 SUCCESS Application created successfully!
💻 Continue Developing
Change directories: cd demo-burns
Deploy: npm run deploy
Now we've got a new directory with our Cloudflare Worker static site template, ready for git. Let's navigate there and see what it created:
cd ~/devel/demo-burns/demo-burns
ls
╭─burns@meg ~/devel/demo-burns/demo-burns ‹main›
╰─➤ ls
AGENTS.md node_modules package.json package-lock.json public wrangler.jsonc
Alright, the two most interesting things for me are this wrangler.jsonc file and the public directory. I have to guess the wrangler file controls our configuration, and the public directory is where my website html is going to go.
wrangler.jsonc
/**
* For more details on how to configure Wrangler, refer to:
* https://developers.cloudflare.com/workers/wrangler/configuration/
*/
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "demo-burns",
"compatibility_date": "2026-03-01",
"assets": {
// The path to the directory containing the `index.html` file to be served at `/`
"directory": "./public"
},
"observability": {
"enabled": true
},
"compatibility_flags": [
"nodejs_compat"
]
}
Pretty straightforward. Things I want to serve up are defined under assets.
What sort of html is the default?
~/devel/demo-burns/demo-burns/public/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello, World!</title>
</head>
<body>
<h1 id="heading">Hello, World!</h1>
<p>This page comes from a static asset stored at `public/index.html` as configured in `wrangler.jsonc`.</p>
</body>
</html>
OK, that ties things together pretty well.
Looking at one more file, let's find out how I run this thing locally. Usually that's in my package.json.
{
"name": "demo-burns",
"version": "0.0.0",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev"
},
"devDependencies": {
"wrangler": "^4.70.0"
}
}
Ok, I can either run npm run dev, or npx wrangler dev. Seems like both will get the job done. Let's try! Both seem to give me the same outcome. A localhost port is listening. Let's click the link!
╰─➤ npm run dev
> demo-burns@0.0.0 dev
> wrangler dev
⛅️ wrangler 4.70.0
───────────────────
Cloudflare collects anonymous telemetry about your usage of Wrangler. Learn more at https://github.com/cloudflare/workers-sdk/tree/main/packages/wrangler/telemetry.md
╭──────────────────────────────────────────────────────────────────────╮
│ [b] open a browser [d] open devtools [c] clear console [x] to exit │
╰──────────────────────────────────────────────────────────────────────╯
⎔ Starting local server...
[wrangler:info] Ready on http://localhost:8787

Oh, I also get some basic logging in my terminal when I connected to that page.
[wrangler:info] GET / 200 OK (7ms)
One more step needed in this config. We may not need this now, but we will need to define the custom domain at some point. Since I'm planning to host this at demo.burns.sh, let's add the following to our wrangler.jsonc.
{
"routes": [
{
"pattern": "demo.burns.sh",
"custom_domain": true
}
]
}
So now our complete file looks like:
/**
* For more details on how to configure Wrangler, refer to:
* https://developers.cloudflare.com/workers/wrangler/configuration/
*/
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "demo-burns",
"compatibility_date": "2026-03-01",
"assets": {
// The path to the directory containing the `index.html` file to be served at `/`
"directory": "./public"
},
"routes": [
{
"pattern": "demo.burns.sh",
"custom_domain": true
}
],
"observability": {
"enabled": true
},
"compatibility_flags": [
"nodejs_compat"
]
}
Alright, that should be everything we really need config-wise.
Next step - we need to push this to GitHub, then configure Cloudflare Workers to pull from GitHub.
Push Wrangler and Site to GitHub
You can create a new repo from the GitHub CLI, but I'll admit I like to do this through the web interface.
Login to GitHub and click New.

Make this private and leave everything blank. We can add more later.

Back on our CLI, we're going to follow the instructions GitHub gave us:
git remote add origin git@github.com:bbbburns/demo-burns.git
git branch -M main
git push -u origin main
Enumerating objects: 11, done.
Counting objects: 100% (11/11), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (11/11), 12.83 KiB | 12.83 MiB/s, done.
Total 11 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:bbbburns/demo-burns.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
Success! Our files exist:

Whoops - look at those commits. That's the initial commit, with no mention of my modified wrangler.jsonc with the custom domain. Let's fix that back on my local machine.
╭─burns@meg ~/devel/demo-burns/demo-burns ‹main*›
╰─➤ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: wrangler.jsonc
no changes added to commit (use "git add" and/or "git commit -a")
╭─burns@meg ~/devel/demo-burns/demo-burns ‹main*›
╰─➤ git commit -a -m "Added custom domain"
[main 4d11088] Added custom domain
1 file changed, 6 insertions(+)
╭─burns@meg ~/devel/demo-burns/demo-burns ‹main›
╰─➤ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 360 bytes | 360.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:bbbburns/demo-burns.git
da0dc98..4d11088 main -> main
That looks better:

Setup Cloudflare to Deploy from GitHub
Ok, we have the world's most basic index.html Hello World website. It builds locally and serves from our laptop. How do we get it pushed out to the world behind one of the largest caching and load balancing systems built.
Let's switch over to our Cloudflare dashboard. Go to Create application under Compute, Workers & Pages.

On the next page we need to Continue with GitHub, and it'll prompt you for authentication as well as permissions to view repos. Since we are using a private repository, we need to give Cloudflare access to it.
The Cloudflare Workers & Pages GitHub App is a good place to check the repo access allowed on the GitHub side.
Here are the permissions we enabled on GitHub.

Then back in our Cloudflare Worker setup, we can see this repo now.

This picks up our deploy command from the package.json command, I assume.

Click Deploy and see what happens!

Very cool. It looks like it took about 31 seconds to globally deploy our website. Is it running?

Woohoo! It's working. Our website is now deployed on a globally scaled infrastructure, maintained by someone else, and even configured with a certificate!
We have paid about $12 and can now use a git push process to get all of our static website assets securely published with zero maintenance overhead.
Updating Our Web Page
Let's talk about updates! This is a pretty boring page. How do we improve it?
With AI! Pause for groans.
Let's ask Claude Code to improve this web page for us. We could do it ourselves, but I have 30 minutes before my next call.
This is a Cloudflare Worker site with static assets in the public folder. I currently have a simple Hello World index.html file, but I'd like to improve this site by having a computer and computer networking technical themed image displayed on the page, overlaid by "Hello World" text. Can you make the smallest set of changes to include an open source image from somewhere like unsplash or pexels and load the image, the modified html, and potentially any css into the "public" folder?
And here's what Claude helped me do:
Done. Two files changed in public/:
- hero.jpg — 206KB server room photo from Unsplash photo by Tyler,https://unsplash.com/photos/a-close-up-of-a-server-in-a-server-room-vSprjjDbu60
- index.html — full-viewport hero image with a green terminal-style "Hello, World!" overlay (monospace font, glow effect, darkened image for contrast)
The CSS is inlined in the `<style>` block so no extra file is needed.
Sources:
- [A close up of a server in a server room — Unsplash](https://unsplash.com/photos/a-close-up-of-a-server-in-a-server-room-vSprjjDbu60)
Thanks for the attribution! Thanks Tyler for the awesome image.
Wait, AI got this wrong. The image it used was actually from Taylor.
Let's see what it looks like locally after just a few seconds of churning away.
npm run dev
[wrangler:info] Ready on http://localhost:8787
[wrangler:info] GET / 200 OK (14ms)
[wrangler:info] GET /hero.jpg 200 OK (3ms)
Not bad! So this works locally.

Here comes the magic part. I update my local site on my laptop, now how do I get this deployed for the whole world to see, and how long will it take?
From the time of my typo-ed git commit message and git push:
commit 622437ea36fab1496e4d62c9b1e10538dc2d74b4 (HEAD -> main, origin/main)
Author: Jason Burns <>
Date: Wed Mar 4 08:43:01 2026 -0500
Added hereo image.
To the time of the build, deployment, and availability:

I have 44 seconds on my stopwatch.
Happy publishing! Share what you create in the comments.