I Exposed My $70 Kubernetes Cluster to the Internet (Without Opening a Single Port)

In my previous post, I talked about how I built a 3-node Kubernetes cluster for ₹6,000 ($70) to host my AI models and home lab. It worked great, but there was one ugly problem: Networking.

The Problem
Accessing my services meant one of two things:

Locally: Typing http://192.168.0.173:30300 for Grafana. Ugly, but works.
Remotely: Setting up WireGuard or Tailscale. Secure, but I can’t ask my friends to install a VPN client just to see a dashboard or chat with my LLM.
I considered opening ports on my router (Port Forwarding) pointing to an Nginx Proxy Manager. Bad idea. Opening ports to the internet is like leaving your front door unlocked because you lost your keys. I didn’t want to wake up to a crypto-miner running on my GTX 1070 Ti.

I needed a solution that was:

  • Secure (No open ports).
  • Free (I’m still on a budget).
  • Public (Accessible via subdomain.domain.com).

Enter Cloudflare Tunnel.

What I Did

  1. The Zero Trust Switch
    I realized Cloudflare provides a “Zero Trust” plan that is completely free for up to 50 users. This includes Tunnels. A tunnel creates an outbound-only connection from your cluster to Cloudflare’s edge network. Traffic flows out to Cloudflare, then back in to your services. No inbound ports needed on my router.

  2. DNS Migration
    My domain (bhargavmantha.dev) was hosting a static site on Netlify. To use Tunnels effectively, I needed Cloudflare to manage the DNS.

  • Old way: Squarespace Registrar -> Netlify DNS.
  • New way: Squarespace Registrar -> Cloudflare DNS -> Netlify (for main site) + Tunnel (for subdomains).

I migrated my nameservers to Cloudflare, which automatically imported my existing records. Zero downtime.

  1. Deploying cloudflared to K8s
    Instead of running a binary on my variety of nodes (Pop!_OS desktop, Ubuntu laptop), I deployed the tunnel connector as a native Kubernetes deployment.

I created a simple manifest
cloudflared.yaml
that takes a token (from the Cloudflare dashboard) and runs 2 replicas for high availability.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: cloudflared
spec:
  replicas: 2
  template:
    spec:
      containers:
      - name: cloudflared
        image: cloudflare/cloudflared:latest
        args:
        - tunnel
        - --no-autoupdate
        - run
        - --token
        - $(TUNNEL_TOKEN)

Now, my cluster dials out to Cloudflare automatically. If I reboot a node, K8s reschedules the pod, and the tunnel reconnects instantly.

  1. Goodbye NodePorts, Hello Subdomains
    This is the magic part. In the Cloudflare Dashboard (Network > Tunnels), I simply mapped public hostnames to my internal ClusterIPs (or NodePorts).
  • grafana.bhargavmantha.dev -> http://192.168.0.173:30300
  • ollama.bhargavmantha.dev -> http://192.168.0.173:31434
  • uptime.bhargavmantha.dev -> http://192.168.0.173:30001

No CLI magic needed—just pointing domains to local IPs

SSL certificates are handled automatically by Cloudflare at the edge. I get the padlock icon without setting up cert-manager or LetsEncrypt inside my cluster.

What I Learned

Lesson 1: Ingress Controllers are Overkill for Homelabs. I spent days trying to get Traefik or Nginx Ingress to play nice with MetalLB on a bare-metal cluster. Cloudflare Tunnel bypassed that entire layer. I don’t need an Ingress Controller; I just need a connector.

Lesson 2: Security can be convenient. By putting everything behind Cloudflare, I can also add an “Access Application” layer later—meaning I can put grafana.bhargavmantha.dev behind a Google/GitHub logic screen, adding 2FA to apps that don’t even support it natively.

Current State
Now my cheap K8s cluster feels like a production cloud environment.

Publicly Accessible: Grafana, Uptime Kuma, and Open WebUI.
Cost: still $0 (Cloudflare Free Tier).
Security: 0 Open Ports.

Leave a Reply