Why nobody outside your network can reach localhost

A webhook provider needs a URL it can POST to, and your server is sitting on localhost:3000. Or a teammate wants to click through the branch you’re running, or your phone needs to hit the dev server to check a layout. The server is up. Nothing outside your network can reach it.

Port forwarding on your router would work, but it exposes your home IP and needs admin access you often don’t have. Cloudflare Tunnel takes the other route. cloudflared, a small daemon you run next to your server, opens an outbound connection to Cloudflare’s edge, and Cloudflare sends traffic back through it. You open no inbound port and you don’t need a public IP.

It comes in two forms. A quick tunnel gives you a public HTTPS URL in one command, on a random hostname that dies when you stop the process. That’s the tool for a five-minute demo or a webhook test. A named tunnel gives you a stable hostname on your own domain that survives restarts, which makes it a small piece of real infrastructure.

How Cloudflare Tunnel works without an open port

The trick is a change of direction. Exposing a service normally means opening an inbound port: something on the internet starts a connection to your machine, so your machine needs a routable address and a firewall rule that lets the traffic in. Blocking exactly that is what NAT and most home and office firewalls are for.

cloudflared never listens for inbound connections. It dials out to Cloudflare’s edge over ordinary outbound HTTPS, the same kind of connection your browser opens all day, and keeps it open. Cloudflare then pushes requests down that connection and reads the responses back, much like a server pushes messages over a WebSocket to a browser that connected first. Your firewall sees one outbound connection it already allows. That’s also why the tunnel behaves the same behind NAT or on a laptop with no public IP: an outbound connection doesn’t care.

The cost is that Cloudflare now sits in the path. Your tunnel hostname resolves to Cloudflare, which decrypts each request at its edge, then re-encrypts it and sends it down the tunnel to your daemon. That’s the same architecture as running behind a CDN or a reverse proxy: you give up direct access in exchange for a middlebox that terminates TLS and can apply access rules before anything reaches your machine.

How to create a quick tunnel with one command

Install cloudflared first: brew install cloudflared on macOS, the .deb/.rpm package from Cloudflare’s repository on Linux, or the Windows installer from the GitHub releases page. Then, with something already listening on a local port:

1
cloudflared tunnel --url http://localhost:3000

A few seconds later you get output like:

1
2
3
4
5
2026-09-23T10:14:02Z INF Thank you for trying Cloudflare Tunnel. Doing so...
2026-09-23T10:14:03Z INF +--------------------------------------------------------------------------------------------+
2026-09-23T10:14:03Z INF |  Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):  |
2026-09-23T10:14:03Z INF |  https://wise-juvenile-pink-recovered.trycloudflare.com                                   |
2026-09-23T10:14:03Z INF +--------------------------------------------------------------------------------------------+

That URL serves HTTPS with a certificate Cloudflare manages, so you never touch TLS configuration. Give it a few seconds to start resolving. From then on, anyone with the link reaches your localhost:3000 until you press Ctrl-C. The hostname is then gone for good, and the next run generates a new one. You don’t log in, create a DNS record, or need an account.

The limits are real, though. The hostname is random, so you can’t put it in a webhook config that has to survive a restart. Cloudflare caps quick tunnels at 200 concurrent requests and doesn’t support Server-Sent Events on them, so a long-lived SSE stream will drop. There’s also no uptime guarantee: Cloudflare’s terms treat quick tunnels as a testing convenience.

For debugging a webhook right now or letting your phone hit the dev server, that’s all you need. If it has to still be running tomorrow, set up a named tunnel.

How to set up a named tunnel on your own domain

A named tunnel needs a free Cloudflare account and a domain whose DNS is on Cloudflare. You do the setup once.

1. Authenticate cloudflared against your account:

1
cloudflared tunnel login

This opens a browser and asks you to pick the domain to authorize. It then saves a certificate at ~/.cloudflared/cert.pem, which every later command uses.

2. Create the tunnel and note its ID:

1
cloudflared tunnel create my-app

This registers a tunnel named my-app and writes a credentials file, <TUNNEL-ID>.json, to ~/.cloudflared/. That file is the tunnel’s identity. Treat it like a secret: anyone holding it can run traffic through your tunnel.

3. Write the config. Create ~/.cloudflared/config.yml:

1
2
3
4
5
6
7
8
9
tunnel: my-app
credentials-file: /home/you/.cloudflared/<TUNNEL-ID>.json

ingress:
  - hostname: app.example.com
    service: http://localhost:3000
  - hostname: api.example.com
    service: http://localhost:8080
  - service: http_status:404

ingress is an ordered list of hostname-to-service rules, matched top to bottom. If you’ve configured nginx as a reverse proxy in front of your apps, it’s the same routing idea written in YAML instead of proxy_pass directives. One tunnel can front several local services on different hostnames. The last rule is mandatory. It’s the catch-all that returns 404 for any hostname the rules above didn’t match, and cloudflared refuses to start without it.

4. Point DNS at the tunnel:

1
cloudflared tunnel route dns my-app app.example.com

This creates a CNAME record for app.example.com pointing at <TUNNEL-ID>.cfargotunnel.com. It’s ordinary DNS resolution, aimed at Cloudflare’s tunnel infrastructure instead of a server IP. Run it once for every hostname in your ingress list.

5. Run it:

1
cloudflared tunnel run my-app

app.example.com now serves whatever listens on localhost:3000, over HTTPS. Cloudflare terminates TLS at the edge, and your origin can stay plain HTTP because the tunnel connection is already encrypted.

A tunnel running in a terminal dies with the terminal. To have it survive a reboot, install it as a system service:

1
sudo cloudflared service install

On Linux, run it with sudo, so $HOME becomes /root and cloudflared can no longer find a config.yml you created as your own user. Point it at the file explicitly instead:

1
sudo cloudflared --config /home/you/.cloudflared/config.yml service install

This copies your config to /etc/cloudflared/config.yml and registers cloudflared with systemd (or launchd, or the Windows service manager), so it starts at boot and restarts if it crashes.

When Cloudflare Tunnel is the wrong choice

A tunnel fits when traffic starts outside your network and has to reach a machine with no public IP, or one you don’t want to expose directly: a laptop, a home server, a box behind corporate NAT. It’s the wrong tool in these cases:

  • You already have a server with a public IP. The tunnel adds Cloudflare’s edge as a hop for no gain. A DNS A record plus your own reverse proxy or TLS termination is simpler and has one less moving part to fail.
  • You need guaranteed low latency or you move large volumes of data. Every byte passes through Cloudflare’s network. For most web traffic that cost is negligible; for high-throughput or latency-sensitive workloads it isn’t.
  • You need the service to stay up when a machine goes down. One cloudflared on one machine is a single point of failure, and service install only covers process restarts. If that host dies, the hostname goes dark. Redundancy means running the same tunnel from a second machine.
  • Your code trusts the TCP source address for security decisions. Requests reach your origin from Cloudflare’s IP ranges. The real client IP arrives in the CF-Connecting-IP header, and anything that reads the socket address will see Cloudflare’s edge instead.

Quick tunnel or named tunnel: which to start with

If you’re not sure which one you need, you need a quick tunnel. Run cloudflared tunnel --url http://localhost:<port> against whatever you’re working on now and open the URL. Move to a named tunnel when you hit one of its two real limits: a hostname that has to outlive the terminal session, or a service that has to keep running unattended.