Who Will Use It Decides Whether You Need Cloudflare Tunnel or an SSH Tunnel

A question that comes up again and again in web development: the service runs on my own machine or inside a private network — how do people or services outside reach it?

Maybe a client needs to see a work-in-progress page, a Stripe webhook has to hit your laptop, or you want to test a PWA on your phone. Maybe a small tool on your home NAS should be public. Or the other way round: you want to reach the production database from your own computer.

Plenty of tools solve these problems — Cloudflare Tunnel, SSH tunnels, Tailscale, ngrok, frp… Their features overlap, but each shines in different situations. This post first explains how the two most common ones (Cloudflare Tunnel and SSH tunnels) work, then walks through use cases one by one: which tool to pick and how to set it up.

The domain in every example is example.com; replace it with your own domain hosted on Cloudflare.


1. What Cloudflare Tunnel is

Cloudflare Tunnel is a reverse-tunnel service from Cloudflare. You run a small program called cloudflared on your machine; it dials out to Cloudflare’s edge and keeps a few persistent connections open. When someone visits your domain, the request reaches Cloudflare first and is then sent back to your machine over those connections.

flowchart LR
    U["User's browser"] -- "HTTPS<br>app.example.com" --> E["Cloudflare edge<br>(CDN, WAF, DDoS protection)"]
    C["cloudflared<br>(your machine)"] == "Outbound tunnel" ==> E
    E -. "Requests come back<br>through the tunnel" .-> C
    C --> S["Local service<br>localhost:8080"]

The connection goes from inside to outside, so the firewall needs no open ports.

This design brings several benefits:

  • No public IP and no port forwarding needed. It works behind NAT, corporate networks and carrier-grade NAT (CGNAT).
  • Your real IP stays hidden; the outside world only sees Cloudflare’s IPs.
  • Cloudflare handles HTTPS certificates, so you don’t run Let’s Encrypt yourself.
  • Traffic passes through Cloudflare’s CDN, WAF and DDoS protection, and you can add Cloudflare Access for login checks.
  • It’s free.

The price: your domain’s DNS must be on Cloudflare, and TLS is terminated at Cloudflare, which means Cloudflare can see the plaintext traffic.

2. The three modes of SSH tunnels

SSH tunnels are OpenSSH’s built-in port forwarding. They work on any machine with SSH and depend on no provider. There are three modes:

Mode Command Purpose
Local forwarding -L ssh -L 5432:localhost:5432 user@server Bring a remote service (e.g. a database) to your machine
Remote forwarding -R ssh -R 8080:localhost:3000 user@vps Publish a local service on a remote host
Dynamic forwarding -D ssh -D 1080 user@server Create a SOCKS proxy; traffic exits via the remote host
flowchart LR
    subgraph L["-L local forwarding: pull remote in"]
        direction LR
        A1["Local program<br>connects to localhost:5432"] --> A2["ssh client"] == "SSH encrypted" ==> A3["Remote sshd"] --> A4["Remote database<br>:5432"]
    end
    subgraph R["-R remote forwarding: push local out"]
        direction LR
        B1["Outside user<br>connects to vps:8080"] --> B2["sshd on the VPS"] == "SSH encrypted" ==> B3["Local ssh client"] --> B4["Local service<br>:3000"]
    end

The difference between -L and -R is who listens: with -L your machine listens; with -R the remote host does.

The mode that overlaps with Cloudflare Tunnel is -R: both dial out from inside to publish a local service. But -R needs a jump host or VPS with a public IP, sshd must have GatewayPorts yes, a domain and HTTPS mean setting up nginx and certbot on the VPS yourself, and reconnecting after a drop relies on autossh.

3. Side-by-side comparison

Aspect SSH tunnel Cloudflare Tunnel
Infrastructure A VPS or jump host with a public IP None — just a domain on Cloudflare
Protocols Any TCP Mainly HTTP/HTTPS; TCP requires cloudflared on the client too
Direction Both (-L pulls in, -R pushes out) Mainly pushes out
Encryption & privacy End-to-end, no third party Decrypted at Cloudflare
HTTPS certificates Set up nginx and certbot yourself Automatic
Exposed IP The VPS’s IP is visible Only Cloudflare’s IPs
DDoS / WAF None Built in
Access control SSH keys (only control who opens tunnels) Cloudflare Access (SSO, email verification)
Reliability autossh reconnects; the VPS is a single point of failure Auto-reconnects to multiple edge locations
Upload limits Only VPS bandwidth 100MB per request on the free plan
Cost VPS monthly fee Free
Vendor lock-in None Tied to Cloudflare

In short, SSH tunnels are lower-level and general-purpose, but you supply the server, certificates and protection yourself. Cloudflare Tunnel does one job — putting HTTP services on the internet — and throws in HTTPS, protection and access control, at the cost of routing traffic through Cloudflare.


4. Choosing by use case

Each case below covers the problem, the recommended solution, why, and the key setup.

Case 1: Quickly showing someone a work in progress

localhost:3000 is running on your machine; a client or colleague should take a look, then you shut it down.

Recommended: Cloudflare Quick Tunnel

No Cloudflare account and no domain needed — just one command:

# Install (Debian/Ubuntu)
curl -L -o cloudflared.deb \
  https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb

# Share local port 3000
cloudflared tunnel --url http://localhost:3000

The terminal prints a URL like https://xxx-yyy-zzz.trycloudflare.com; send it to the other person. Press Ctrl+C and the URL stops working.

Limits: the URL changes every time, concurrent requests are capped, and Server-Sent Events are not supported. Use it for quick demos only, never in production.

If you’d rather install nothing, SSH-based services work the same way:

ssh -R 80:localhost:3000 localhost.run

Case 2: Webhook development and testing on a real phone

You’re building a LINE bot, Stripe payments or a GitHub App, and the provider needs a fixed HTTPS callback URL. Or you need to test camera, geolocation or PWA features on a phone, which require HTTPS (a secure context).

Recommended: a Cloudflare Named Tunnel with a fixed subdomain such as dev.example.com

A Quick Tunnel URL changes on every restart, so you’d have to update the webhook settings each time. A Named Tunnel’s URL is fixed: set it up once, and from then on you just start cloudflared each day to receive webhooks. Setup is covered in the next case.

If you need to inspect and replay individual requests while debugging, ngrok’s built-in inspector is handier, and the two can be used side by side.

Case 3: Deploying an app listening on a port to a subdomain

A program on the machine listens on port 8080; you want it at https://app.example.com, running reliably for the long term.

Recommended: Cloudflare Named Tunnel + systemd

This is the textbook use of Cloudflare Tunnel. With the domain already on Cloudflare, the first setup takes about five minutes:

# 1. Authorise (opens a browser; pick example.com)
cloudflared tunnel login

# 2. Create a tunnel (one per machine is enough; it can serve many apps)
cloudflared tunnel create my-server

# 3. DNS: app.example.com → this tunnel (creates the CNAME for you)
cloudflared tunnel route dns my-server app.example.com

# 4. Run it
cloudflared tunnel run --url http://localhost:8080 my-server

Open https://app.example.com and the service is there, with HTTPS already working.

Several services on one machine

One tunnel can route requests to different ports by hostname:

flowchart LR
    U1["app.example.com"] --> E["Cloudflare"]
    U2["api.example.com"] --> E
    U3["admin.example.com"] --> E
    E == "One tunnel" ==> C["cloudflared<br>ingress rules"]
    C --> S1["localhost:8080<br>Frontend"]
    C --> S2["localhost:3000<br>API"]
    C --> S3["localhost:9000<br>Admin"]

One tunnel, one cloudflared process, three services routed by hostname.

Put the rules in ~/.cloudflared/config.yml:

tunnel: my-server
credentials-file: /home/me/.cloudflared/<TUNNEL-ID>.json   # created in step 2

ingress:
  - hostname: app.example.com
    service: http://localhost:8080
  - hostname: api.example.com
    service: http://localhost:3000
  - hostname: admin.example.com
    service: http://localhost:9000
  - service: http_status:404          # the last rule must be a catch-all
# Each new subdomain needs its DNS record once
cloudflared tunnel route dns my-server api.example.com
cloudflared tunnel route dns my-server admin.example.com

# Check the configuration
cloudflared tunnel ingress validate

# Install as a system service that starts on boot
sudo cloudflared --config ~/.cloudflared/config.yml service install
sudo systemctl restart cloudflared

Adding a service later takes three steps: add a rule to config.yml, run route dns once, restart cloudflared.

You can also skip the CLI: create the tunnel in the Cloudflare dashboard under Zero Trust → Networks → Tunnels, run sudo cloudflared service install <TOKEN> on the server, and manage subdomains on the web from then on. With Docker, the cloudflare/cloudflared image drops straight into docker-compose as one more service.

Common problems

  1. Only one level of subdomain. The free Universal SSL certificate covers example.com and *.example.com, so app.example.com works, but dev.app.example.com gets a certificate error unless you pay for an Advanced Certificate.
  2. DNS record conflicts. If the subdomain already has an A or CNAME record, route dns fails. Delete the old record first, or add -f to overwrite it.
  3. The framework rejects the Host header. Many dev servers only accept localhost by default; add the domain to the allow list:
    • Vite: server.allowedHosts: ['app.example.com']
    • Django: ALLOWED_HOSTS = ['app.example.com']
    • Rails: config.hosts << "app.example.com"
    • webpack dev server: allowedHosts: 'all'
  4. The app itself serves HTTPS with a self-signed certificate. Change service to https://localhost:8443 and add
    originRequest:
      noTLSVerify: true
  5. 502 Bad Gateway. Usually the local service isn’t running or the port is wrong. Run curl http://localhost:8080 on the machine first to confirm it responds.
  6. The app sees Cloudflare’s IP for every visitor. The real IP is in the CF-Connecting-IP header.

Case 4: Internal tools and staging, for specific people only

Grafana, Jenkins or an admin panel for colleagues; a staging site for a client to sign off — but not open to everyone.

Recommended: Cloudflare Tunnel + Cloudflare Access

After connecting the service to a subdomain as in Case 3, go to Zero Trust → Access → Applications, create an application for that subdomain and set who’s allowed — for example, emails ending in @example.com, a few specific addresses, or Google or GitHub logins.

sequenceDiagram
    participant U as User
    participant A as Cloudflare Access
    participant T as Tunnel
    participant S as Internal tool
    U->>A: Open grafana.example.com
    A-->>U: Not signed in, redirect to login
    U->>A: Sign in with an email code or Google
    A->>A: Check against the allow list
    A-->>U: Issue a session cookie
    U->>A: Request again with the cookie
    A->>T: Verified, forward the request
    T->>S: localhost:3000
    S-->>U: Return the page

Authentication happens at Cloudflare; requests that fail it never reach your machine.

The upside: no code changes, no VPN, and no SSH keys to hand out to non-technical people. The free plan covers 50 users.

If team members need long-lived connections between several private machines, and not just over HTTP (SSH, databases, file sharing), a mesh VPN such as Tailscale is more direct: once each machine runs Tailscale, they behave as if they were on the same LAN.

Case 5: Reaching a remote database from your machine

The production PostgreSQL only listens on 127.0.0.1, and you want to query it with a GUI tool on your laptop.

Recommended: SSH -L local forwarding

This is home turf for SSH tunnels. Cloudflare Tunnel is good at pushing out; this case is about pulling in:

ssh -N -L 15432:localhost:5432 user@db-server

Then point the GUI tool at localhost:15432. -N means no shell, forwarding only.

If you do this often, put it in ~/.ssh/config:

Host prod-db
    HostName db-server.example.com
    User deploy
    LocalForward 15432 localhost:5432

After that, ssh -N prod-db is all it takes.

Related uses: adding -L during a debugging session to look at a remote admin page that only listens on localhost, or ssh -D 1080 to create a SOCKS proxy so the browser goes out through the remote host.

Case 6: No port 22 open, but SSH still needed

You want every port on the server’s firewall closed, SSH included.

Recommended: run SSH through Cloudflare Tunnel and gate it with Access

Add an ingress rule that carries SSH into the tunnel:

ingress:
  - hostname: ssh.example.com
    service: ssh://localhost:22
  # ...other rules
  - service: http_status:404

The client also needs cloudflared; add this to ~/.ssh/config:

Host ssh.example.com
    ProxyCommand cloudflared access ssh --hostname %h

Now ssh [email protected] connects through Cloudflare. Configure Access for ssh.example.com and a browser SSO check runs before the connection opens. The server exposes no ports at all, and who may log in is managed centrally in Access.

This also shows the two tools aren’t an either-or: guard the front door with Cloudflare, then use SSH -L to the database once you’re inside. They stack.

Case 7: When Cloudflare Tunnel is the wrong fit

Some requirements don’t suit routing traffic through Cloudflare:

Requirement Recommended Why
Traffic must not be decrypted by a third party (finance, healthcare, contractual limits) SSH tunnels, or self-hosted frp or WireGuard End-to-end encryption; nobody in the middle can read it
Large uploads (over 100MB) or video streaming SSH -R + VPS, or host directly on a VPS Avoids the size cap and Cloudflare’s terms of service
Non-HTTP protocols for ordinary users (game servers, MQTT, custom TCP) Self-hosted frp, or open the port on a VPS Cloudflare Tunnel’s TCP mode needs cloudflared on every client
Extremely latency-sensitive Host directly on a VPS, or a direct Tailscale connection One less relay hop
No vendor lock-in at all SSH tunnels, frp Standard open-source tools that work anywhere

5. Summary

Tools by use case

Category Use case Recommended
Publishing web services Quick demo Cloudflare Quick Tunnel
Webhook development, testing on a phone Cloudflare Named Tunnel (fixed subdomain)
Small sites and side projects in production Named Tunnel + systemd
Home server, NAS, no fixed IP Cloudflare Tunnel
Several services on one machine Tunnel + config.yml ingress
Specific people only Internal tools, admin panels Tunnel + Access
Staging for client sign-off Tunnel + Access (specific emails)
Long-lived links between team machines Tailscale
Engineer access Remote database or Redis SSH -L
Ad-hoc debugging SSH -L / -R
SOCKS proxy SSH -D
Server with port 22 closed Tunnel + Access in front of SSH
Special requirements No third-party decryption SSH tunnels, frp, WireGuard
Large files, video streaming Host directly on a VPS
Publishing non-HTTP protocols frp, open port on a VPS
Inspecting and replaying requests ngrok

Decision flow

flowchart TD
    Q1{"Will people use it<br>in a browser?"}
    Q1 -- Yes --> Q2{"Is routing traffic<br>through Cloudflare OK?"}
    Q1 -- "No, engineers or programs" --> Q5{"What do you need?"}
    Q2 -- No --> X1["SSH -R + VPS<br>or self-hosted frp"]
    Q2 -- Yes --> Q3{"Only temporary?"}
    Q3 -- Yes --> T1["Quick Tunnel"]
    Q3 -- "No, long-term or fixed URL" --> T2["Named Tunnel<br>app.example.com"]
    T2 --> Q4{"Restrict who can see it?"}
    Q4 -- Yes --> T3["Add Cloudflare Access"]
    Q4 -- No --> T4["Publish openly"]
    Q5 -- "Pull a remote service in" --> S1["SSH -L"]
    Q5 -- "Link machines long-term" --> S2["Tailscale"]
    Q5 -- "Publish a non-HTTP protocol" --> S3["frp or open a VPS port"]

Ask who will use it first, then whether traffic may go through Cloudflare; two or three questions settle most cases.

Closing thoughts

If your domain is already on Cloudflare, a Cloudflare Named Tunnel covers most everyday web development and small deployments: one tunnel serving several subdomains, Access added where needed, and not a single port open on the server. The remaining pull-in needs, such as reaching a remote database, go to SSH -L.

Only when traffic must not pass through a third party, when large files are involved, or when a non-HTTP protocol has to be public do you need a VPS, frp or Tailscale. The rule of thumb is simple: for services people reach in a browser, choose Cloudflare Tunnel; for engineers or programs using arbitrary protocols, or traffic that must not pass through a third party, choose SSH tunnels or a self-hosted option.