But I need a static IP and my ISP put me behind a double NAT

Series: You Don’t Need a VPC | Post 2 of 5


The Old Way

The classic home server setup involves:

  1. Forwarding ports 80/443 on your router
  2. Setting up Let’s Encrypt and auto-renewing certs
  3. Using a DDNS service because your ISP gives you a dynamic IP
  4. Hoping your ISP doesn’t block inbound port 80
  5. Accepting that your home IP is now publicly exposed

It works, but it’s fragile and exposes your home network’s surface area directly.


Enter Cloudflare Tunnel (wth it’s free??)

Cloudflare Tunnel (cloudflared) runs as a container on your home server and creates an outbound-only connection to Cloudflare’s edge. No inbound ports. No exposed IP. No cert management.

cloudflared:
  image: cloudflare/cloudflared:latest
  command: tunnel --no-autoupdate run
  environment:
    - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
  restart: unless-stopped

Traffic flow: user → cloudflare.com → tunnel → your nginx → service

Your home IP is never in the picture.

Oh, and it’s free. DDoS protection comes along for the ride.


Managing DNS with Terraform

Once you have a tunnel, you need DNS records. Doing this manually doesn’t scale past a few services. Terraform keeps it all in code:

# Public services — proxied through Cloudflare (tunnel)
resource "cloudflare_record" "wildcard_public" {
  zone_id = var.cloudflare_zone_id
  name    = "*"
  value   = "${var.tunnel_id}.cfargotunnel.com"
  type    = "CNAME"
  proxied = true
}
 
# LAN services — DNS only, points to home IP via DDNS
resource "cloudflare_record" "wildcard_local" {
  zone_id = var.cloudflare_zone_id
  name    = "*.local"
  value   = var.home_ip
  type    = "A"
  proxied = false
}

One wildcard for tunnel traffic, one for direct LAN access. Both managed in the same terraform apply.


When Tunnels Break Down

One real limitation: Cloudflare Tunnel is HTTP/TCP only. WebRTC TURN servers need UDP port 3478, which tunnels don’t support.

For LiveKit, the TURN server gets its own DNS record with proxied = false, pointing directly to the home IP. The DDNS updater keeps it current. It’s a small crack in the “no exposed ports” model, but scoped to exactly one service.


Next Up

Post 3: Running the whole stack without root — rootless Podman + SELinux on Fedora.