DEV Community

Cover image for Managing Multiple Services with Cloudflare Local Tunneling
Rahul Ravindran
Rahul Ravindran

Posted on • Originally published at rahulr.cc on

Managing Multiple Services with Cloudflare Local Tunneling

Introduction

Every developer eventually runs into the same problem.

You have an application running locally. Maybe it's a ReactJS frontend on port 3000, a ExpressJS backend on port 8000, a Grafana dashboard on port 3001, or a Home Assistant instance running inside your homelab.

Everything works perfectly on your machine.

Then someone asks:

"Can I take a look?"

Suddenly you're dealing with router configuration, port forwarding, firewall rules, dynamic IP addresses, SSL certificates, and security concerns.

Historically, exposing local services to the internet required opening inbound ports on your network and routing traffic directly to internal systems. While this works, it introduces complexity and risk.

This is where Cloudflare Tunnel changes the game.

Instead of opening your network to incoming traffic, a lightweight daemon called cloudflared creates an outbound connection to Cloudflare's network. Traffic reaches Cloudflare first and is then securely forwarded through the tunnel back to your local service.

The result is:

  • No port forwarding

  • No public IP requirements

  • Automatic HTTPS

  • Better security

  • Easy access control

  • Multiple services behind a single tunnel

In this guide, we'll build a production-style setup capable of exposing multiple local applications through a single Cloudflare Tunnel.

By the end, you'll be able to access services like:

app.example.com
api.example.com
grafana.example.com

Enter fullscreen mode Exit fullscreen mode

all through one tunnel connection.


Understanding Cloudflare Tunnel

Before writing commands, let's understand what's happening behind the scenes.

Traditional hosting looks like this:

Internet
   |
Router
   |
Port Forward
   |
Local Service

Enter fullscreen mode Exit fullscreen mode

Cloudflare Tunnel works differently:

Internet
   |
Cloudflare Edge
   |
Encrypted Tunnel
   |
cloudflared
   |
Local Services

Enter fullscreen mode Exit fullscreen mode

Notice the important difference.

Your local machine never accepts inbound internet traffic.

Instead:

  1. cloudflared creates an outbound encrypted connection

  2. Cloudflare maintains that connection

  3. Requests travel through the existing tunnel

  4. Your service responds securely

This significantly reduces attack surface.


Real-World Example

Imagine you're developing a SaaS platform.

Locally you have:

Service

|

Port

Frontend

|

3000

|
|

API

|

8080

|
|

Grafana

|

3001

|

Without Cloudflare Tunnel:

  • Forward 3 ports

  • Configure reverse proxy

  • Manage certificates

  • Handle DNS manually

With Cloudflare Tunnel:

  • One tunnel

  • Three DNS records

  • Automatic HTTPS

Much simpler.


Prerequisites

You'll need:

For examples we'll use:

example.com

Enter fullscreen mode Exit fullscreen mode

Replace this with your actual domain.


Installing cloudflared

Ubuntu / Debian

curl -L \
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb \
-o cloudflared.deb

sudo dpkg -i cloudflared.deb

Enter fullscreen mode Exit fullscreen mode

Verify installation:

cloudflared --version

Enter fullscreen mode Exit fullscreen mode

Example output:

cloudflared version 2025.x.x

Enter fullscreen mode Exit fullscreen mode

macOS

Using Homebrew:

brew install cloudflared

Enter fullscreen mode Exit fullscreen mode

Verify:

cloudflared --version

Enter fullscreen mode Exit fullscreen mode

Windows

Using winget:

winget install Cloudflare.cloudflared

Enter fullscreen mode Exit fullscreen mode

Verify:

cloudflared --version

Enter fullscreen mode Exit fullscreen mode

Note: In future, if any of these methods aren't working you can refer to the up to date installation guide by Cloudflare


Authenticating with Cloudflare

Next we connect cloudflared to our Cloudflare account.

Run:

cloudflared tunnel login

Enter fullscreen mode Exit fullscreen mode

Browser opens automatically.

Select:

Your Account
→ Your Domain
→ Authorize

Enter fullscreen mode Exit fullscreen mode

Once complete, cloudflared downloads a certificate file.

Typical location:

~/.cloudflared/cert.pem

Enter fullscreen mode Exit fullscreen mode

This file authorizes tunnel creation.


Creating Your First Tunnel

Create a named tunnel:

cloudflared tunnel create homelab

Enter fullscreen mode Exit fullscreen mode

Output:

Created tunnel homelab
Tunnel ID:
6f5c1a90-xxxx-xxxx-xxxx

Enter fullscreen mode Exit fullscreen mode

Cloudflared also generates credentials.

Example:

~/.cloudflared/6f5c1a90.json

Enter fullscreen mode Exit fullscreen mode

This file identifies your tunnel.

Keep it safe.


Creating DNS Routes

Connect DNS records to the tunnel.

Frontend:

cloudflared tunnel route dns homelab app.example.com

Enter fullscreen mode Exit fullscreen mode

API:

cloudflared tunnel route dns homelab api.example.com

Enter fullscreen mode Exit fullscreen mode

Grafana:

cloudflared tunnel route dns homelab grafana.example.com

Enter fullscreen mode Exit fullscreen mode

Cloudflare automatically creates the required DNS records.

No manual configuration needed.


Running Sample Services

Let's simulate a realistic environment.

Frontend

npm run frontend

Enter fullscreen mode Exit fullscreen mode

Accessible locally:

http://localhost:3000

Enter fullscreen mode Exit fullscreen mode

API

npm run api

Enter fullscreen mode Exit fullscreen mode

Dashboard

npm run grafana

Enter fullscreen mode Exit fullscreen mode

Now we have three applications.


Creating the Configuration File

Create:

mkdir -p ~/.cloudflared
nano ~/.cloudflared/config.yml

Enter fullscreen mode Exit fullscreen mode

Add:

tunnel: 6f5c1a90-xxxx-xxxx
credentials-file: /home/user/.cloudflared/6f5c1a90.json

ingress:

  - hostname: app.example.com
    service: http://localhost:3000

  - hostname: api.example.com
    service: http://localhost:8080

  - hostname: grafana.example.com
    service: http://localhost:3001

  - service: http_status:404

Enter fullscreen mode Exit fullscreen mode

Let's break this down.


Tunnel ID

tunnel: 6f5c1a90-xxxx

Enter fullscreen mode Exit fullscreen mode

Specifies which tunnel cloudflared should use.


Credentials

credentials-file:

Enter fullscreen mode Exit fullscreen mode

Points to the authentication file.


Ingress Rules

These are routing rules.

Think of them like Nginx virtual hosts.

Example:

- hostname: api.example.com
  service: http://localhost:8080

Enter fullscreen mode Exit fullscreen mode

Translation:

When traffic arrives for api.example.com, send it to localhost:8080.


Catch-All Rule

Always end with:

- service: http_status:404

Enter fullscreen mode Exit fullscreen mode

Without it, unmatched requests can behave unexpectedly.


Starting the Tunnel

Run:

cloudflared tunnel run homelab

Enter fullscreen mode Exit fullscreen mode

Expected logs:

Connected to Cloudflare
Registered tunnel connection

Enter fullscreen mode Exit fullscreen mode

At this point:

https://app.example.com
https://api.example.com
https://grafana.example.com

Enter fullscreen mode Exit fullscreen mode

should all work.


Understanding Ingress Matching

Rules are evaluated top to bottom.

Example:

ingress:

  - hostname: app.example.com
    service: http://localhost:3000

  - hostname: "*.example.com"
    service: http://localhost:5000

  - service: http_status:404

Enter fullscreen mode Exit fullscreen mode

Request:

app.example.com

Enter fullscreen mode Exit fullscreen mode

matches first rule.

Request:

test.example.com

Enter fullscreen mode Exit fullscreen mode

matches wildcard rule.

Order matters.


Real Homelab Example

Many self-hosters run:

grafana.example.com
prometheus.example.com
portainer.example.com
jellyfin.example.com

Enter fullscreen mode Exit fullscreen mode

using a single tunnel:

ingress:

  - hostname: grafana.example.com
    service: http://localhost:3000

  - hostname: prometheus.example.com
    service: http://localhost:9090

  - hostname: portainer.example.com
    service: http://localhost:9443

  - hostname: jellyfin.example.com
    service: http://localhost:8096

  - service: http_status:404

Enter fullscreen mode Exit fullscreen mode

One tunnel.

Multiple services.

No port forwarding.


Troubleshooting

Tunnel Not Starting

Validate config:

cloudflared tunnel ingress validate

Enter fullscreen mode Exit fullscreen mode

Useful before every deployment.


View Logs

cloudflared tunnel run homelab --loglevel debug

Enter fullscreen mode Exit fullscreen mode

Provides detailed diagnostics.


Verify DNS

dig app.example.com

Enter fullscreen mode Exit fullscreen mode

Expected:

CNAME

Enter fullscreen mode Exit fullscreen mode

pointing to Cloudflare's tunnel endpoint.


Check Local Service

Always verify:

curl http://localhost:3000

Enter fullscreen mode Exit fullscreen mode

before blaming the tunnel.

Most issues originate from the service itself.


Running as a Service

Instead of manually launching cloudflared:

sudo cloudflared service install

Enter fullscreen mode Exit fullscreen mode

Enable:

sudo systemctl enable cloudflared

Enter fullscreen mode Exit fullscreen mode

Start:

sudo systemctl start cloudflared

Enter fullscreen mode Exit fullscreen mode

Check status:

sudo systemctl status cloudflared

Enter fullscreen mode Exit fullscreen mode

Now the tunnel survives reboots.


Conclusion

You've built a secure multi-service gateway without opening a single inbound port.

Using one Cloudflare Tunnel, we've:

  • Installed cloudflared

  • Authenticated with Cloudflare

  • Created a named tunnel

  • Added DNS routes

  • Configured multiple services

  • Implemented ingress routing

  • Enabled automatic HTTPS

  • Prepared the tunnel for production use

Top comments (0)