DEV Community

Cover image for Cloudflare Tunnel on Multiple VPSes: HA, Scaling, and Pitfalls
Rahul Ravindran
Rahul Ravindran

Posted on • Originally published at rahulr.cc on

Cloudflare Tunnel on Multiple VPSes: HA, Scaling, and Pitfalls

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

No issues.


Directus CMS (Self-hosted)

Directus requires shared backend infrastructure.

Good:

 VPS1 Directus
 VPS2 Directus
       |
       |
Shared PostgreSQL

Enter fullscreen mode Exit fullscreen mode

Bad:

VPS1 -> PostgreSQL A
VPS2 -> PostgreSQL B

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

are counted separately.


Scenario 2: Same Tunnel, Completely Different Services

Suppose:

VPS1
 ├─ Express
 ├─ Directus
 └─ Umami

VPS2
 ├─ Grafana
 └─ Prometheus

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Cloudflare sees:

Tunnel
├─ Connector VPS1
└─ Connector VPS2

Enter fullscreen mode Exit fullscreen mode

A request arrives:

https://app.example.com

Enter fullscreen mode Exit fullscreen mode

Cloudflare may choose either connector.

Case A:

Cloudflare
    |
   VPS1
    |
 Express

Enter fullscreen mode Exit fullscreen mode

Success.

Case B:

Cloudflare
    |
   VPS2
    |
 Express ?

Enter fullscreen mode Exit fullscreen mode

Container doesn't exist.

Result:

502 Bad Gateway

Enter fullscreen mode Exit fullscreen mode

or

Connection Refused

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Server B:

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

Enter fullscreen mode Exit fullscreen mode

But both use:

same tunnel UUID
same credentials file

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

Traffic can arrive at either connector.


Recommended Architecture for Different Services

Instead of:

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

Enter fullscreen mode Exit fullscreen mode

Use:

Tunnel A Tunnel B
   | |
 VPS1 VPS2

Enter fullscreen mode Exit fullscreen mode

Example:

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

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

And:

Shared PostgreSQL
Shared Redis
Shared Object Storage

Enter fullscreen mode Exit fullscreen mode

Architecture:

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

Enter fullscreen mode Exit fullscreen mode

This can work surprisingly well for small and medium deployments.


Scenario 5: Docker External Networks Across VPSes

Many people create:

networks:
  tunnel:
    external: true

Enter fullscreen mode Exit fullscreen mode

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

This works:

VPS1
 ├─ cloudflared
 ├─ express
 └─ directus

Enter fullscreen mode Exit fullscreen mode

because all containers share the same Docker network.

This does not work:

VPS1 cloudflared
      |
      |
      X
      |
      |
VPS2 directus

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Architecture:

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Monitoring:

Separate Tunnel
       |
    Grafana
   Prometheus

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)