Problem
Each proxy currently targets a single container/backend. If a service is scaled horizontally (multiple container replicas), there is no way to distribute traffic across them. The PortConfig struct in internal/model/port.go already stores targets []*url.URL — a slice — but the reverse proxy always uses GetFirstTarget() in internal/proxymanager/port.go, ignoring all other targets.
This makes TSDProxy unsuitable for scaled-out services unless you run a separate load balancer in front (defeating the simplicity goal).
Proposed Solution
Multi-target support with load balancing
Allow multiple target URLs per port with a configurable balancing strategy:
# Docker labels — comma-separated or additional labels
tsdproxy.targets: "container1:3000,container2:3000,container3:3000"
# Or via port label with multiple targets
tsdproxy.port.1: "443/https:3000/http"
tsdproxy.port.1.target: "container2:3000"
tsdproxy.port.1.target: "container3:3000"
Or in the List provider:
proxies:
myapp:
ports:
"443/https":
targets:
- "http://container1:3000"
- "http://container2:3000"
- "http://container3:3000"
loadbalance: "roundrobin"
Load balancing strategies
| Strategy |
Description |
roundrobin |
Distributes requests sequentially across targets (default) |
leastconn |
Sends to target with fewest active connections |
first |
Always uses the first healthy target (active/passive failover) |
random |
Random selection |
Health-aware routing
When a target is unhealthy (existing health checker detects it), it should be drained from the pool until it recovers. This builds on the existing health check infrastructure in internal/proxymanager/health.go.
Implementation Notes
- The
targetState struct in internal/model/port.go already supports multiple targets — the data model is ready
- The
ReverseProxy in internal/proxymanager/port.go needs a custom Transport or director that selects a target per-request
- A
loadbalance field should be added to PortConfig
- For HTTP, a custom
httputil.ReverseProxy director can select a target per-request
- For TCP, a connection-level balancer is needed (round-robin across backend connections)
- UDP is connectionless but per-client affinity could be supported via consistent hashing
- Health check integration: the existing
HealthResult can be extended to track per-target health
Scope
Phase 1: Round-robin for HTTP/HTTPS targets (covers 80% of use cases)
Phase 2: Least-connections + health-aware draining
Phase 3: TCP/UDP load balancing
Alternatives
- Run a separate load balancer (Traefik, nginx, HAProxy) in front of TSDProxy — adds complexity and another service to manage
- Use Docker Swarm/K8s built-in load balancing (only works within the orchestrator)
- Tailscale's own load balancing (not available via tsnet)
Problem
Each proxy currently targets a single container/backend. If a service is scaled horizontally (multiple container replicas), there is no way to distribute traffic across them. The
PortConfigstruct in internal/model/port.go already storestargets []*url.URL— a slice — but the reverse proxy always usesGetFirstTarget()in internal/proxymanager/port.go, ignoring all other targets.This makes TSDProxy unsuitable for scaled-out services unless you run a separate load balancer in front (defeating the simplicity goal).
Proposed Solution
Multi-target support with load balancing
Allow multiple target URLs per port with a configurable balancing strategy:
Or in the List provider:
Load balancing strategies
roundrobinleastconnfirstrandomHealth-aware routing
When a target is unhealthy (existing health checker detects it), it should be drained from the pool until it recovers. This builds on the existing health check infrastructure in internal/proxymanager/health.go.
Implementation Notes
targetStatestruct in internal/model/port.go already supports multiple targets — the data model is readyReverseProxyin internal/proxymanager/port.go needs a customTransportor director that selects a target per-requestloadbalancefield should be added toPortConfighttputil.ReverseProxydirector can select a target per-requestHealthResultcan be extended to track per-target healthScope
Phase 1: Round-robin for HTTP/HTTPS targets (covers 80% of use cases)
Phase 2: Least-connections + health-aware draining
Phase 3: TCP/UDP load balancing
Alternatives