Exposing Homelab through Cloudflare Tunnel
Source: Dev.to
I love self‑hosting. I run my own blog, dashboards, and random experiments from my own server. What I don’t love is port forwarding, ISP NAT, dynamic IPs, and exposing my router to the entire Internet.
Thankfully, Cloudflare has a solution for this called Cloudflare Tunnel. It lets me expose services publicly without opening a single inbound port on my router. This post is how I actually use it in production.
The Problem with “Traditional” Self‑Hosting
The classic approach looks like this:
flowchart LR
Internet --> Router
Router --> Server
That means:
- Open ports on the router
- Trust your firewall configuration
- Hope the IP doesn’t change
- Become a soft DDoS target
For a homelab or personal server, this is a recipe for disaster and it isn’t a scalable solution.
The Cloudflare Tunnel Approach
With Cloudflare Tunnel the direction changes. Instead of the Internet coming into my network, my server reaches out.
flowchart LR
User --> Cloudflare
Cloudflare --> Tunnel
Tunnel --> Server
Now my server only makes outbound connections to Cloudflare, and Cloudflare sits in the front as the public edge. This is much more secure: there are no open ports and no public IP is needed, removing an entire category of risk.
What Actually Runs on My Server?
On my server I run a small daemon called cloudflared.
flowchart TB
cloudflared --> CloudflareEdge
CloudflareEdge --> LocalService1
CloudflareEdge --> LocalService2
cloudflared then:
- Opens an encrypted tunnel to Cloudflare
- Authenticates using my account credentials
- Routes traffic internally to
localhost
Therefore my apps never touch the public Internet directly.
Installing cloudflared
On Linux it takes about a minute to install cloudflared:
curl -fsSL https://pkg.cloudflare.com/install.sh | sudo bash
sudo apt install cloudflared
Quick sanity check:
cloudflared --version
Note: It’s often better to download the binary from the official website and install it manually; this way you control updates.
Authenticating My Server
To authenticate the server, run:
cloudflared tunnel login
A browser window opens, prompting you to log in to your Cloudflare account, select a domain, and approve the connection. Credentials are stored locally in ~/.cloudflared/cert.pem for future tunnels.
Creating the Tunnel
Create a tunnel with:
cloudflared tunnel create
Cloudflare returns a UUID for the tunnel and a URL to download a configuration file. Save that file as ~/.cloudflared/.json. This file is the identity of the server.
My Tunnel Configuration (Example)
A simplified version of the configuration file:
tunnel:
credentials-file: ~/.cloudflared/.json
ingress:
- hostname: blog.example.com
service: http://localhost:8080
- hostname: dashboard.example.com
service: http://localhost:8081
- service: http_status:404
This creates one tunnel that routes multiple hostnames to different services on the server, with a safe fallback (http_status:404).
How Traffic Flows End‑to‑End
Full picture:
flowchart LR
User --> DNS
DNS --> Cloudflare
Cloudflare --> Tunnel
Tunnel --> ReverseProxy
ReverseProxy --> App
Conceptually:
sequenceDiagram
participant User
participant Cloudflare
participant Tunnel
participant Server
User->>Cloudflare: HTTPS request
Cloudflare->>Tunnel: Forward request
Tunnel->>Server: Local HTTP
Server-->>Tunnel: Response
Tunnel-->>Cloudflare: Encrypted response
Cloudflare-->>User: HTTPS response
From the outside it behaves like a normal HTTPS connection, but the traffic is encrypted end‑to‑end, and on the server it appears as localhost traffic.
DNS: No Manual Records Needed
Cloudflare Tunnel automatically creates the necessary DNS records. It adds a CNAME that points your domain to Cloudflare’s edge. Run:
cloudflared tunnel route dns
…and that’s it! Cloudflare handles the rest.
Running the Tunnel Permanently
First test the tunnel:
cloudflared tunnel run
When it works, install it as a system service:
sudo cloudflared service install
sudo systemctl enable cloudflared
sudo systemctl start cloudflared
Now it starts on boot, restarts on failure, and runs as a non‑root user.
Adding Authentication
For anything private (admin panels, dashboards), I add Cloudflare Access policies to require authentication before the request reaches the tunnel. This ensures that only authorized users can reach those services.
Cloudflare Access
Which lets me:
- Require login before accessing the service
- Restrict by email or identity provider
- Protect internal tools without VPNs
So from the outside, my dashboard looks public, but in reality it’s locked behind authentication.
My Production Setup
This is the layout I actually recommend:
flowchart LR
User --> Cloudflare
Cloudflare --> Tunnel
Tunnel --> ReverseProxy
ReverseProxy --> Blog
ReverseProxy --> Dashboard
ReverseProxy --> Admin
Why this works so well?
Cloudflare handles edge security, while my reverse proxy handles routing. The apps stay internal and “dumb,” and there are zero inbound ports.
Common Mistakes I’ve Made (So You Don’t Have To)
- Exposing apps directly instead of through a reverse proxy
- Forgetting a 404 ingress rule
- Running admin panels without Access
- Binding services to
0.0.0.0unnecessarily instead oflocalhost
Rule of thumb:
If it’s not meant to be public, keep it onlocalhost. If it’s meant to be public, use Cloudflare Access.
Final Thoughts
While Cloudflare Tunnel is a powerful tool, it’s not a silver bullet. It’s just one piece of the puzzle. Combine it with other security measures—like Cloudflare Access—to keep your server secure.
Don’t use it for services such as:
- Latency‑sensitive services
- Game servers
- Full‑network access (I use WireGuard for that)
For HTTP(S) apps, however, it’s my default. For personal servers and homelabs, this is one of the cleanest setups you can run today.