If you've ever self-hosted anything from home — a Gitea instance, a Home Assistant dashboard, a personal API — you've run into the same wall: your ISP router,…
If you've ever self-hosted anything from home — a Gitea instance, a Home Assistant dashboard, a personal API — you've run into the same wall: your ISP router, CGNAT, or plain old paranoia about port forwarding. Opening 443 to the world from your home network is an attack surface you have to babysit forever. Cloudflare Tunnel solves this by flipping the connection: your server reaches out to Cloudflare instead of waiting for the world to reach in. No open ports, no dynamic DNS hacks, and you get Cloudflare's network sitting in front of your app for free.
This matters more now than it used to, because CGNAT is increasingly the default on residential and mobile ISPs across Africa and elsewhere — port forwarding simply doesn't work for a growing number of home connections. A tunnel sidesteps that entirely.
What you'll need
- A domain added to Cloudflare (free plan is fine)
- A Linux server or machine to host
cloudflared — this can be a Raspberry Pi, a VPS, or your home desktop
- Root or sudo access on that machine
- An app already running locally that you want to expose (we'll use a simple example on
localhost:3000)
- About 20 minutes
Step 1: Install cloudflared
On Debian/Ubuntu:
curl -L https://pkg.cloudflare.com/cloudflare-main.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloudflare-main.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install cloudflared
On other distros, grab the binary directly from Cloudflare's releases and drop it in /usr/local/bin:
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
chmod +x cloudflared-linux-amd64
sudo mv cloudflared-linux-amd64 /usr/local/bin/cloudflared
Confirm it's working:
cloudflared --version
Step 2: Authenticate with Cloudflare
cloudflared tunnel login
This opens a browser window (or gives you a URL to paste if you're headless) asking you to pick the domain you want to use. Once authorized, it drops a certificate at ~/.cloudflared/cert.pem. That file is your account credential for creating tunnels — treat it like a password.
Step 3: Create the tunnel
cloudflared tunnel create home-server
This generates a tunnel ID and a credentials JSON file in ~/.cloudflared/. Note the tunnel ID — you'll need it in the config.
Step 4: Write the config file
Create ~/.cloudflared/config.yml:
tunnel: home-server
credentials-file: /home/youruser/.cloudflared/<TUNNEL_ID>.json
ingress:
- hostname: app.yourdomain.com
service: http://localhost:3000
- hostname: files.yourdomain.com
service: http://localhost:8080
- service: http_status:404
That last http_status:404 catch-all is not optional — cloudflared refuses to start without a default rule for traffic that doesn't match any hostname.
Step 5: Route DNS
Instead of manually creating CNAME records, let the CLI do it:
cloudflared tunnel route dns home-server app.yourdomain.com
cloudflared tunnel route dns home-server files.yourdomain.com
Check the Cloudflare DNS dashboard — you'll see a CNAME pointing to <TUNNEL_ID>.cfargotunnel.com, proxied (orange cloud on).
Step 6: Run it
Test it live first:
cloudflared tunnel run home-server
Hit app.yourdomain.com from your phone on mobile data (not your home wifi, or you won't actually prove anything). If it loads, you're done with the fun part.
Step 7: Make it a service
Ctrl+C the manual run and install it properly so it survives reboots:
sudo cloudflared service install
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared
By default this reads the config from /etc/cloudflared/config.yml, so copy yours there:
sudo mkdir -p /etc/cloudflared
sudo cp ~/.cloudflared/config.yml /etc/cloudflared/
sudo cp ~/.cloudflared/<TUNNEL_ID>.json /etc/cloudflared/
Update the credentials-file path in the copied config to match.
What actually goes wrong
"Error 1033" in the browser. Almost always means the tunnel isn't running, or the DNS CNAME wasn't created correctly. Run cloudflared tunnel info home-server to check connector status.
Local service is up but tunnel returns 502. Your service: URL in the ingress config almost certainly doesn't match how the app is actually listening. If your app binds to 127.0.0.1:3000 but something else grabbed the port, or the app only listens on IPv6, cloudflared will fail silently-ish. Confirm with curl http://localhost:3000 from the same machine before blaming the tunnel.
Config changes don't take effect. If you're running as a systemd service, it reads /etc/cloudflared/config.yml, not your home directory copy. Edit the wrong file and wonder why nothing changed — everyone does this once.
WebSocket apps (like some dashboards or chat apps) drop connections. Add originRequest: { noTLSVerify: true } or check that your app doesn't require sticky sessions across multiple tunnel replicas — this only matters if you scale to multiple cloudflared instances.
Credentials file permissions. If you copied the JSON credentials file as root but the service runs as another user (or vice versa), you'll get cryptic auth failures. Keep ownership consistent — chown it to whichever user actually runs the service.
Security notes
Cloudflare Tunnel doesn't make your app secure by itself — it just hides your IP and removes the need for an open port. You still want:
- Cloudflare Access in front of anything sensitive (internal dashboards, admin panels) — it's free for small teams and adds a login wall before traffic ever reaches your app
- TLS termination handled by Cloudflare, but don't assume that means your backend doesn't need HTTPS internally too if multiple services talk to each other
- Rate limiting or WAF rules on the Cloudflare dashboard for anything public-facing, since your app is now reachable by anyone who finds the hostname
Wrapping up
Once this is running, the natural next step is layering Cloudflare Access on top for anything you don't want fully public — think a self-hosted Gitea or a personal admin panel. It takes about five more minutes and turns "anyone with the URL" into "anyone I've explicitly allowed," without touching your app's own auth system at all.
Further reading:
Hashtags
#cloudflaretunnel #selfhosting #cloudflared #homelab #reverseproxy #devops