Monaf Horany
All runbooks

Runbooks

Publishing a Next.js app through a Cloudflare Tunnel

Serving a self-hosted app on a real domain without opening a single inbound port — and the resolver quirk that made it look broken when it was not.

CloudflarecloudflaredNext.jssystemd4 steps · 20–30 min

Addresses in this guide are placeholders in 10.0.0.0/24 and the domain is example.dev. Everything else is verbatim from the working notes.

  1. 01

    Install and authenticate

    The tunnel dials out to Cloudflare and traffic returns down that connection, so the origin needs no port forwarding and no public address at all.

    bash
    wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
    sudo dpkg -i cloudflared-linux-amd64.deb
    
    cloudflared login
  2. 02

    Create the tunnel and point it at the app

    bash
    cloudflared tunnel create nextjs-tunnel
    /etc/cloudflared/config.yml
    yaml
    tunnel: nextjs-tunnel
    credentials-file: /home/master/.cloudflared/<tunnel-id>.json
    
    ingress:
      - hostname: example.dev
        service: http://10.0.0.80:30000
      - service: http_status:404
  3. 03

    Route DNS and run it as a service

    Delete any existing A/AAAA/CNAME record for the hostname first; the route command creates the CNAME it needs and will not overwrite a conflicting record.

    bash
    cloudflared tunnel route dns nextjs-tunnel example.dev
    
    sudo cloudflared service install
    sudo systemctl restart cloudflared
  4. 04

    The resolver fix

    On the origin host the tunnel hostname resolved to a local IPv6 address, so requests from the machine itself never left the box — while every external client worked fine. Pinning the resolver fixed it.

    /etc/systemd/resolved.conf
    ini
    [Resolve]
    DNS=1.1.1.1
    FallbackDNS=8.8.8.8
    bash
    sudo systemctl restart systemd-resolved
    curl -v https://example.dev