At the end of Part I, the VM is running. k3s is up. Twenty pods are humming along on a machine under my desk. But the VM sits behind libvirt NAT at 192.168.122.x, on a desktop with no static public IP. Nobody can reach it.
I replaced the entire traditional ingress stack with one pod.
Bridging the Gap
Without a public IP, something has to bridge the gap between the internet and a VM on a NAT network. The options:
Port forwarding + dynamic DNS. Forward ports through the host into the VM's NAT network, register a dynamic DNS name, run cert-manager for TLS. Only works if your ISP doesn't use CGNAT—and even then, you're exposing your home IP, managing iptables rules across two NAT layers, and hoping your address doesn't rotate while cert-manager tries to validate a challenge. Fragile and exposed.
VPS relay. Rent a cheap VPS with a static IP, WireGuard from the cluster to it, reverse proxy on the VPS. More control, more to maintain, and a monthly cost for what amounts to a relay.
Tailscale Funnel / ngrok. Same pattern as Cloudflare Tunnel—outbound connection to a publicly reachable relay. Tailscale Funnel is the closest equivalent and a solid option.
Every alternative follows the same shape: connect outbound to something publicly reachable. Cloudflare Tunnel is the version where that relay also gives you TLS termination, automatic DNS records, and a security layer you didn't ask for but won't refuse. DDoS protection applies to all inbound traffic automatically. WAF rules and rate limiting sit at the edge before a single byte reaches the cluster. The attack surface shrinks to one daemon that only makes outbound connections—there's nothing to port scan, nothing for Shodan to index. If Cloudflare itself is compromised, my personal website is not the thing I'm worried about.
Rather than fighting NAT, I leaned into the constraint.
One Pod
The concept is disarmingly simple. Instead of opening ports and routing traffic inward, you run a cloudflared daemon inside the cluster that dials out to Cloudflare's edge. Cloudflare receives requests for your domain, sends them through the tunnel, and the daemon proxies to the right internal service. Traffic flows outbound-only. Your cluster never opens a port.
The routing configuration is a ConfigMap:
tunnel: 86ffa052-fefa-4ec1-ba6a-28cfc1c5efa0credentials-file: /etc/cloudflared/credentials.jsonmetrics: 0.0.0.0:2000ingress:- hostname: www.crowprose.comservice: http://www-web.default.svc.cluster.local:80- hostname: crowprose.comservice: http://www-web.default.svc.cluster.local:80- hostname: app.crowprose.comservice: http://app-web.default.svc.cluster.local:80- hostname: api.crowprose.comservice: http://api-service.default.svc.cluster.local:3000- hostname: o11y.crowprose.comservice: http://monitoring-grafana.monitoring.svc.cluster.local:80- service: http_status:404
Five hostnames. One tunnel. The catch-all returns 404 for anything unmatched. The services are standard ClusterIP—no NodePort, no LoadBalancer type, nothing exposed beyond the cluster network.
Here's what's not in my cluster as a result:
No ingress controller. No NGINX pods consuming 100m+ CPU to watch Ingress resources and reload config files. No Traefik. No envoy sidecars. The cloudflared ConfigMap is the routing table.
No cert-manager. Cloudflare terminates TLS at the edge. The tunnel itself is encrypted end-to-end. You never provision, renew, or debug a certificate. If you've ever had cert-manager silently fail to renew and learned about it from a browser warning, you understand why this matters.
No DNS management. Cloudflare creates the DNS records for tunnel hostnames automatically. One less thing to fat-finger.
The entire ingress layer is a single pod requesting 25m CPU and 64Mi memory. I've seen ingress controllers that use more resources than the applications behind them.
The Maintenance Page
I self-host on a desktop. I restart it—kernel updates, k3s bumps, the occasional "I wonder if a reboot fixes this." Every time, the cloudflared pod dies and Cloudflare's default error page takes over. It's generic, branded with Cloudflare's logo, and makes a planned five-minute restart look like the site is fundamentally broken.
The fix is a Cloudflare Worker—a few lines of JavaScript at the edge that intercepts 502 and 503 responses before they reach the visitor. When the tunnel is down, it returns a self-contained HTML page with inline CSS and no external dependencies. When the tunnel recovers, the Worker transparently steps aside. The whole file is under a hundred lines.
It's the kind of detail that doesn't matter until it does. The difference between "this site is down" and "this site is being maintained" is entirely vibes, and vibes are free at the Cloudflare edge.
Tradeoffs
Cloudflare dependency. If Cloudflare goes down, I go down. For a personal project, I genuinely do not care. For something with an SLA, you'd want a fallback.
No remote cluster access. There's no VPN or bastion host—I can't kubectl from a coffee shop. No tailing logs, no exec-ing into pods, no manual intervention unless I'm at my desk. But deploying works from anywhere: push to main, GitHub Actions builds the images, and FluxCD on the cluster pulls the changes. The deploy path is entirely git-based and never touches the cluster API. And Grafana is exposed through the tunnel at o11y.crowprose.com, so I have observability from any browser.
The operational gap is solvable if I want it. Tailscale would give me encrypted access to the cluster API from anywhere—same outbound-only pattern as the Cloudflare Tunnel. A WireGuard tunnel to the host would work too. For now, git-push deploys and browser-based monitoring cover what I actually need.
Tunnel latency. Every request takes an extra hop through Cloudflare's nearest edge node. For a static site and a REST API, the added milliseconds are invisible. For latency-sensitive WebSocket workloads, you'd want to measure it.
For twenty pods—three Postgres databases, an API gateway with typed SDK generation, AI agents, a sharded tables service, a full Prometheus and Grafana stack, and GitOps pulling it all together—a single Cloudflare Tunnel is the entire ingress layer. Which is to say: barely any.