VOOZH about

URL: https://dev.to/imrhlrvndrn/cloudflare-tunnel-on-multiple-vpses-ha-scaling-and-pitfalls-4k0d

⇱ Cloudflare Tunnel on Multiple VPSes: HA, Scaling, and Pitfalls - DEV Community


Introduction

Cloudflare Tunnel is one of the simplest ways to expose self-hosted applications without opening inbound ports on a VPS. A common question arises when infrastructure grows beyond a single server:

What happens if I run the same Cloudflare Tunnel on multiple VPSes?

Can it be used for high availability? Does it automatically become a load balancer? Can different servers expose different services through the same tunnel? What breaks when applications become stateful?

This article explores these questions in depth and covers several real-world deployment scenarios.


Understanding Cloudflare Tunnel Connectors

A Cloudflare Tunnel consists of:

  • A Tunnel ID (UUID)

  • Tunnel credentials

  • One or more cloudflared connector instances

A single tunnel may have multiple active connectors.

For example:

 Cloudflare Edge
 |
 Tunnel: prod-tunnel
 |
 +------------+------------+
 | |
 Connector A Connector B
 VPS1 VPS2

Both VPSes establish outbound connections to Cloudflare. No inbound ports are required.


Common Misconception: "Multiple Connectors = Failover Only"

Many administrators assume:

VPS1 active
VPS2 standby

and that Cloudflare only switches to VPS2 when VPS1 dies. That is not entirely accurate. Cloudflare can utilize multiple healthy connectors attached to the same tunnel simultaneously.

Conceptually:

Request 1 -> VPS1
Request 2 -> VPS2
Request 3 -> VPS1
Request 4 -> VPS2

However, this should not be confused with a dedicated load balancer.

Cloudflare Tunnel does not provide:

  • Weighted traffic distribution

  • Service-level health checks

  • Session affinity

  • Canary deployments

  • Geographic routing

  • Blue-green deployments

Those features belong to Cloudflare Load Balancing.


Scenario 1: Identical Services on Both VPSes

Consider:

VPS1
 ├─ Express API
 ├─ Directus
 └─ Umami

VPS2
 ├─ Express API
 ├─ Directus
 └─ Umami

Both servers:

  • Run the same tunnel

  • Use the same tunnel credentials

  • Use the same ingress configuration

This setup works well.

 Client
 |
 Cloudflare
 |
 Tunnel
 |
 +------+------+
 | |
 VPS1 VPS2

Traffic can reach either VPS. This is the closest thing to horizontal scaling that Tunnel provides by itself.


The Hidden Requirement: Shared State

Most scaling failures occur because applications are not truly stateless.

Express API

Usually safe if:

  • Stateless

  • JWT authentication

  • External session storage

Example:

VPS1 -> Express
VPS2 -> Express

No issues.


Directus CMS (Self-hosted)

Directus requires shared backend infrastructure.

Good:

 VPS1 Directus
 VPS2 Directus
 |
 |
Shared PostgreSQL

Bad:

VPS1 -> PostgreSQL A
VPS2 -> PostgreSQL B

Data immediately diverges.


Any self-hosted analytics services

Same requirement. Both instances should point to the same database.

Otherwise:

Visitors on VPS1
Visitors on VPS2

are counted separately.


Scenario 2: Same Tunnel, Completely Different Services

Suppose:

VPS1
 ├─ Express
 ├─ Directus
 └─ Umami

VPS2
 ├─ Grafana
 └─ Prometheus

and both use the same tunnel credentials. At first glance this seems convenient. Unfortunately, it introduces a major problem.


Why Different Services Break Shared Tunnels

Assume the tunnel config contains:

ingress:
 - hostname: app.example.com
 service: http://express:3000

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

Cloudflare sees:

Tunnel
├─ Connector VPS1
└─ Connector VPS2

A request arrives:

https://app.example.com

Cloudflare may choose either connector.

Case A:

Cloudflare
 |
 VPS1
 |
 Express

Success.

Case B:

Cloudflare
 |
 VPS2
 |
 Express ?

Container doesn't exist.

Result:

502 Bad Gateway

or

Connection Refused

This is one of the most common tunnel architecture mistakes.


Important Rule

Every connector attached to a tunnel should be able to satisfy every ingress rule associated with that tunnel.

If not:

  • Routing becomes unpredictable

  • Some requests fail

  • Troubleshooting becomes difficult


Scenario 3: Same Tunnel Credentials, Different Config Files

Another subtle case:

Server A:

ingress:
 - hostname: app.example.com
 service: http://express:3000

Server B:

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

But both use:

same tunnel UUID
same credentials file

This configuration is dangerous. Cloudflare does not maintain a mapping of:

app.example.com -> VPS1 only
grafana.example.com -> VPS2 only

The tunnel is the routing object. The connector is merely an endpoint attached to that tunnel. As a result:

Cloudflare
 |
 Tunnel
 |
+---+---+
| |
A B

Traffic can arrive at either connector.


Recommended Architecture for Different Services

Instead of:

One Tunnel
 |
+---+---+
| |
VPS1 VPS2

Use:

Tunnel A Tunnel B
 | |
 VPS1 VPS2

Example:

prod-tunnel
 ├─ app.example.com
 ├─ cms.example.com
 └─ analytics.example.com

monitoring-tunnel
 ├─ grafana.example.com
 └─ prometheus.example.com

This is cleaner, safer, and easier to operate.


Scenario 4: Using Tunnel as a Horizontal Scaling Mechanism

Can Tunnel provide horizontal scaling? Technically yes, but only under certain conditions.

Requirements:

VPS1
 ├─ Express
 ├─ Directus
 └─ Umami

VPS2
 ├─ Express
 ├─ Directus
 └─ Umami

And:

Shared PostgreSQL
Shared Redis
Shared Object Storage

Architecture:

 Cloudflare
 |
 Tunnel
 |
 +-----------+-----------+
 | |
 VPS1 VPS2
 | |
 +-----------+-----------+
 |
 Shared Database

This can work surprisingly well for small and medium deployments.


Scenario 5: Docker External Networks Across VPSes

Many people create:

networks:
 tunnel:
 external: true

and assume it can span servers. It cannot. Docker bridge networks are local to a host.

This works:

VPS1
 ├─ cloudflared
 ├─ express
 └─ directus

because all containers share the same Docker network.

This does not work:

VPS1 cloudflared
 |
 |
 X
 |
 |
VPS2 directus

Docker networking does not magically connect containers across hosts.

You need:

  • Overlay networking

  • Kubernetes

  • Docker Swarm

  • Tailscale

  • WireGuard

  • Service mesh

or another cross-host networking solution.


Scenario 6: Real Load Balancing

Eventually you may need:

  • Health checks

  • Weighted traffic

  • Regional routing

  • Traffic steering

  • Session stickiness

At that point use:

Cloudflare Load Balancer

Architecture:

 Client
 |
 Cloudflare LB
 |
+------+------+
| |
VPS1 VPS2

Unlike Tunnel alone, the Load Balancer actively understands backend health and routing policies.


Recommended Production Architecture

For a modern self-hosted stack:

VPS1
 ├─ cloudflared
 ├─ Express
 ├─ Directus

VPS2
 ├─ cloudflared
 ├─ Express
 ├─ Directus

Shared:
 ├─ PostgreSQL
 ├─ Redis
 └─ Object Storage

Monitoring:

Separate Tunnel
 |
 Grafana
 Prometheus

This provides:

  • High availability

  • Connector redundancy

  • Basic traffic distribution

  • Horizontal scaling

  • Simpler operations

without introducing unnecessary complexity.


Final Takeaway

Cloudflare Tunnel is excellent for exposing services securely and providing connector-level redundancy. Multiple connectors attached to the same tunnel can improve availability and distribute traffic, but they are not a replacement for a dedicated load balancer.

The most important rule is simple:

If multiple servers share a tunnel, every server should be capable of serving every hostname defined by that tunnel.

If servers host different workloads, create separate tunnels.

If servers host identical workloads backed by shared state, multiple connectors can provide a lightweight and effective high-availability architecture.