So turns out I misunderstood how Consul is meant to be used. Instead of having downstream applications connecting directly to a centralized Consul cluster, you’re meant to have Consul running on all servers/instances in client mode. You configure your downstream application to connect to the local Consul, which in turn connects to your Consul servers.
Funnily enough, Consul in client mode is sort of doing what I was unsuccessfully trying to get Haproxy to do (ie, forward requests to my Consul servers and continue working if any one of the servers is down).
The hoops I’ve had to jump through to get things working in Docker Swarm is another story, but my questions in this post no longer need answering