Host a website from your own machine with Cloudflare Tunnel
Move your domain to Cloudflare, create a tunnel, and serve apps running on your own hardware without opening a single port.
Some projects are too heavy for a cheap VPS: ML models, data pipelines, search clusters, anything that needs a lot of RAM, a GPU, or CPU time. You might already have a machine at home that can run them. The problem is getting the internet to it safely.
The usual answer is to open ports on your router, find a static IP, and manage TLS certificates yourself.
Cloudflare Tunnel skips all of that. A small program called cloudflared runs on your machine and makes an
outbound connection to Cloudflare. Visitors hit Cloudflare, and Cloudflare sends their requests down that tunnel
to your local app.
visitor ──https──▶ Cloudflare edge ──tunnel──▶ cloudflared (your machine) ──▶ http://localhost:3000
What you get:
- No open ports. Your router stays closed. The connection starts from inside your network.
- No static IP needed. It works behind CGNAT, a dynamic IP, even mobile hotspots.
- Your home IP stays hidden. DNS points at Cloudflare, never at you.
- Free HTTPS. Cloudflare issues and renews the certificate.
This guide goes from "I bought a domain" to "my app is live on it", step by step.
What you need
- A domain name from any registrar (Namecheap, GoDaddy, Porkbun, …).
- A free Cloudflare account.
- A Linux machine (Debian/Ubuntu commands below) that stays on and runs your app on a local port, e.g. a
web app on
localhost:3000. sudoaccess on that machine.
Throughout this post, replace example.com with your domain and home-server with whatever you want to call your
tunnel.
Step 1: Move your domain's DNS to Cloudflare
You don't have to move the registration to use Cloudflare. You only need Cloudflare to answer DNS for the domain, which you do by changing its nameservers. Your registrar still bills you for renewals as before.
- In the Cloudflare dashboard, click Add a domain, enter
example.com, and pick the Free plan. - Cloudflare scans your existing DNS records and imports them. Review the list. Anything you rely on today,
especially email (
MXandTXTrecords), must be there before you switch. - Cloudflare shows you two nameservers, something like
name1.ns.cloudflare.comandname2.ns.cloudflare.com. - If DNSSEC is on at your registrar, turn it off first. Leaving it on while the nameservers change can take the domain offline. You can turn it back on later from Cloudflare's DNS settings.
- At your registrar, find the domain's nameserver setting and switch it to custom nameservers. On Namecheap that's Domain List → Manage → Nameservers → Custom DNS. Other registrars have an equivalent screen. Paste in Cloudflare's two nameservers and save.
- Wait. Cloudflare emails you when the domain becomes Active. It's often done within an hour, but can take up to 24.
Check from any terminal:
dig NS example.com +short
# name1.ns.cloudflare.com.
# name2.ns.cloudflare.com.
When both answers are Cloudflare's, you're in.
Optional: transfer the registration to Cloudflare Registrar
With the nameservers moved, you can also move the registration itself. Cloudflare Registrar charges the wholesale price, with no markup, and includes WHOIS privacy. It's optional; everything else in this guide works either way.
- The domain must already be Active on Cloudflare (step 1).
- At your current registrar, unlock the domain and request the authorisation (EPP) code.
- In Cloudflare, go to Domain Registration → Transfer Domains, select the domain, and enter the code.
- Pay for the transfer. It adds one year to your registration, so nothing is wasted.
- Approve the confirmation email from your old registrar, or wait for it to time out, which usually takes up to 5 days.
Two rules to know:
- The 60-day lock. You can't transfer a domain within 60 days of registering it or of a previous transfer.
- TLD support. Not every top-level domain can be transferred to Cloudflare. The transfer page tells you if yours can't.
Step 2: Tidy up DNS
Open DNS → Records for the domain.
- Keep your email records. If you use email forwarding or a mail provider, leave its
MXandTXT(SPF, DKIM) records alone. They should stay DNS only (grey cloud). - Delete stale
NSrecords. Imports sometimes bring inNSrecords pointing at your old registrar's DNS servers. They do nothing useful inside a Cloudflare zone and can confuse tools, so remove them. - Don't create
Arecords for your home IP. The tunnel creates the records you need in step 4. Pointing DNS at your home IP would expose it, which is exactly what the tunnel avoids.
Step 3: Install cloudflared
On the machine that runs your app, add Cloudflare's package repository and install:
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
| sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' \
| sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt-get update && sudo apt-get install -y cloudflared
Or grab the .deb directly:
curl -L -o cloudflared.deb \
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
Check it:
cloudflared --version
The apt route is better long term, because apt upgrade keeps cloudflared current.
Step 4: Create the tunnel
Log in
cloudflared tunnel login
This prints a URL. Open it in a browser, log in to Cloudflare, and pick your domain. cloudflared saves a
certificate to ~/.cloudflared/cert.pem, which lets it manage tunnels and DNS for that domain.
Create it
cloudflared tunnel create home-server
This registers the tunnel with Cloudflare and writes a credentials file to
~/.cloudflared/<TUNNEL-UUID>.json. Treat that file like a password: anyone who has it can run your tunnel. Never
commit it to git.
Confirm it exists:
cloudflared tunnel list
Point your hostnames at it
cloudflared tunnel route dns home-server example.com
cloudflared tunnel route dns home-server www.example.com
Each command creates a proxied CNAME to <TUNNEL-UUID>.cfargotunnel.com. In the dashboard these show up with the
type Tunnel and the tunnel's name. Pointing the bare domain (example.com) at a CNAME is normally not allowed;
Cloudflare handles it with CNAME flattening, so it just works.
Add a line for every hostname you want to serve, for example api.example.com.
Step 5: Tell the tunnel where to send traffic
The ingress rules in a config file map incoming hostnames and paths to local services. Create
~/.cloudflared/config.yml:
tunnel: <TUNNEL-UUID>
credentials-file: /home/you/.cloudflared/<TUNNEL-UUID>.json
ingress:
# API requests on the main domain go to a separate backend
- hostname: example.com
path: ^/api/
service: http://localhost:8080
# Everything else on the main domain and www goes to the website
- hostname: example.com
service: http://localhost:3000
- hostname: www.example.com
service: http://localhost:3000
# A whole subdomain for another app
- hostname: app.example.com
service: http://localhost:9000
# Required catch-all: anything that matched nothing above
- service: http_status:404
How the rules work:
-
Top to bottom, first match wins. Put specific rules (with a
path) above general ones for the same hostname. -
pathis a regular expression, not a glob.^/api/matches paths that start with/api/. Without the^it would match/api/anywhere in the path. -
The last rule must have no
hostname. It catches everything else.http_status:404is the usual choice. -
servicecan be any local URL: a different port, a different machine on your LAN, orhttps://for a local service with its own certificate. -
If a local service uses a self-signed HTTPS certificate, add this under that rule:
originRequest: noTLSVerify: trueOnly use it for local self-signed certificates. Plain
http://localhostdoesn't need it.
Check your rules before running anything:
cloudflared tunnel ingress validate
cloudflared tunnel ingress rule https://example.com/api/users # which rule catches this URL?
Then test in the foreground:
cloudflared tunnel run home-server
Open https://example.com. If your app loads, the tunnel works. Stop it with Ctrl+C and make it permanent.
Step 6: Run it as a service
As a systemd service, the tunnel starts on boot and restarts if it crashes. The service reads its config from
/etc/cloudflared/, so copy the files there first:
sudo mkdir -p /etc/cloudflared
sudo cp ~/.cloudflared/config.yml /etc/cloudflared/config.yml
sudo cp ~/.cloudflared/<TUNNEL-UUID>.json /etc/cloudflared/
Edit /etc/cloudflared/config.yml so credentials-file points at the new location:
credentials-file: /etc/cloudflared/<TUNNEL-UUID>.json
Lock the files down, then install and start the service:
sudo chmod 600 /etc/cloudflared/config.yml /etc/cloudflared/<TUNNEL-UUID>.json
sudo cloudflared service install
sudo systemctl enable --now cloudflared
Check that it's healthy:
systemctl status cloudflared # should say "active (running)"
cloudflared tunnel info home-server # should list active connections
journalctl -u cloudflared -f # live logs
After any change to /etc/cloudflared/config.yml, run sudo systemctl restart cloudflared.
Step 7: Verify and harden
Check that traffic really flows through Cloudflare:
curl -sI https://example.com | grep -i '^server'
# server: cloudflare
Then a few habits that keep a home server safe:
- Only route what you mean to publish. Every ingress rule is a door to the internet. Never point a hostname at a database admin panel, a dashboard, or anything else meant only for you. When you add or change a rule, double-check which local service that port actually belongs to.
- Put private tools behind Cloudflare Access. For internal dashboards you still want to reach from outside, create an Access application under Zero Trust → Access. Visitors must then log in with an email code or SSO before the request ever reaches your machine.
- Bind local services to
127.0.0.1where you can. The tunnel reaches them fine, and nothing else on your network can. - Turn on "Always Use HTTPS" under SSL/TLS → Edge Certificates.
- Keep cloudflared updated with
apt upgrade.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Error 1033 | The tunnel isn't connected | systemctl status cloudflared; check the logs |
| 502 Bad Gateway | cloudflared is up, but nothing answers on the local port | Is the app running? Is the port in config.yml correct? Try curl localhost:3000 on the machine |
| 404 from your site's domain | No ingress rule matched | cloudflared tunnel ingress rule https://… shows which rule a URL hits |
| Domain doesn't resolve | Nameservers not active yet | dig NS example.com +short; wait, or re-check the registrar settings |
TLS errors to a local https:// service | Self-signed certificate | Add noTLSVerify: true for that rule only |
Recap
- Point your domain's nameservers at Cloudflare (and optionally transfer the registration).
- Clean up DNS: keep email records, drop stale ones, and never expose your home IP.
- Install cloudflared, then
tunnel login→tunnel create→tunnel route dns. - Map hostnames and paths to local ports in
config.yml, ending with a 404 catch-all. - Run it as a systemd service, verify it, and only expose what you mean to.
Your home machine now serves the internet over HTTPS, with no open ports and no public IP, and it can run workloads far heavier than a small VPS could afford.
Connected
shares a topic with this postStreamlit Portfolio Template
A portfolio and resume website written entirely in Python. Edit one data file, run one command, and you have a dark-themed, animated personal site with a working contact form.
Botly: a local RAG chatbot
A private chatbot that answers from your own PDFs. It runs entirely on your machine with Ollama, LangChain, FAISS and Streamlit, packaged in one Docker image.