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
- Trusting your firewall configuration
- Hoping the IP doesn’t change
- Becoming a soft DDoS target
For a homelab or personal server, this is a recipe for disaster, and it’s not a scalable solution.
The Cloudflare Tunnel Approach #
With Cloudflare Tunnel, the direction changed. Instead of the internet coming into my network, it’s my server that reaches out.
flowchart LR User --> Cloudflare Cloudflare --> Tunnel Tunnel --> Server
This way my server only makes outbound connections to Cloudflare, and Cloudflare sits in the front as the public edge. Therefore it’s much more secure, as there are on open ports and no public IP needed. This alone removes 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:
Keep in mind that it is better to download the binary from the official website and install it manually, as this way it would be updated automatically.
Authenticating my server #
In order to authenticate my server, I need to run the following command:
This will open a browser and ask me to log in to my Cloudflare account. After that, it would prompt me to select my domain and approve the connection. Credentials are then stored locally in ~/.cloudflared/cert.pem for future tunnels.
Creating the Tunnel #
In order to create the tunnel, I need to run the following command:
cloudflared tunnel create <tunnel-name>
Cloudflare will then give me a UUID for the tunnel, and a URL to download the configuration file. I need to save this configuration file in ~/.cloudflared/<tunnel-name>.json. This would be the identity of the server.
My Tunnel Configration (Or an example of it) #
Here’s a simplified version of my configuration file:
tunnel: <tunnel-name>
credentials-file: ~/.cloudflared/<tunnel-name>.json
ingress:
- hostname: blog.example.com
service: http://localhost:8080
- hostname: dashboard.example.com
service: http://localhost:8081
- service: http_status:404
This would create one tunnel, that has multiple domains pointing to different services running on my server. And it’s a clean separation between services, and also adds a safe dafault fallback with http_status:404
How Traffic Flows End-to-End #
This is the full picture:
flowchart LR User --> DNS DNS --> Cloudflare Cloudflare --> Tunnel Tunnel --> ReverseProxy ReverseProxy --> App
Or, 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 from my server’s perspective, it’s just localhost traffic.
DNS: No manual records needed #
Cloudflare Tunnel automatically handles DNS records for you. It creates a CNAME record for your domain that points to Cloudflare’s edge. To do that, I run the following command:
cloudflared tunnel route dns <tunnel-name> <domain>
And that’s it! Cloudflare will handle the rest.
Running the Tunnel Permanently #
I first test the tunnel by running:
cloudflared tunnel run <tunnel-name>
Once it works, I 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. 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? Well, Cloudflare would handle edge security, whereas my reverse proxy would handle routing, apps will 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.0 unnecessarily instead of localhost
If it’s not meant to be public, keep it on localhost. 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. Make sure to use it in combination with other security measures, like Cloudflare Access, to keep your server secure.
However, it shouldn’t be used for some services like:
- Latency sensitive services
- Game Servers
- Full network access (I use WireGuard for that)
For HTTP(S) apps though, it’s my default. As for personal servers and homelabs, this is one of the cleanest setups you can run today.