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.
