Does Caddy cache DNS entries when using reverse proxy with Docker Services?

1. The problem I’m having:

Hi!

By default, when a service is scaled in Docker Compose using the deploy.replicas property (as can be seen in the example in the docker-compose.yml file), multiple instances of the same service are created and registered in the Docker DNS server with the same domain name but pointing to different IPs.

In this scenario, when configuring a reverse proxy to that service in Caddy Server, it is usually done as follows:

:80 {
    reverse_proxy whoami:8000
}

With that configuration, when several consecutive calls are made to the final service through Caddy Server (calling localhost:8080), the requests are always resolved towards the same service and the same IP is always used.

~ ❯ curl http://localhost:8080 
I'm fa9fca8a5293

~ ❯ curl http://localhost:8080
I'm fa9fca8a5293

~ ❯ curl http://localhost:8080
I'm fa9fca8a5293

~ ❯ curl http://localhost:8080
I'm fa9fca8a5293

~ ❯ curl http://localhost:8080
I'm fa9fca8a5293

~ ❯ curl http://localhost:8080
I'm fa9fca8a5293

~ ❯ curl http://localhost:8080
I'm fa9fca8a5293

On the other hand, if a dynamic upstream is used, as shown in the following configuration, the requests are balanced between the different instances created.

:80 {
    reverse_proxy {
      lb_policy round_robin
      dynamic a {
        name whoami
        port 8000
        refresh 1s
      }
    }
}
~ ❯ curl http://localhost:8080
I'm ece316ecfe32

~ ❯ curl http://localhost:8080
I'm b5cfbdb0ce4c

~ ❯ curl http://localhost:8080
I'm 8c63835eace1

~ ❯ curl http://localhost:8080
I'm b5cfbdb0ce4c

~ ❯ curl http://localhost:8080
I'm ece316ecfe32

~ ❯ curl http://localhost:8080
I'm b5cfbdb0ce4c

~ ❯ curl http://localhost:8080
I'm b5cfbdb0ce4c

The question is: when simply using reverse_proxy whoami:8000, does Caddy Server cache the first IP returned by the DNS server and does not make a balancing between the different entries that are registered in the Docker DNS server? To make a balancing between the different instances the only solution is to use a dynamic upstream? Services like Nginx by default perform that balancing between all DNS entries for a service, hence my question.

Thank you very much.

2. Error messages and/or full log output:

N/A

3. Caddy version:

v2.8.4 running in docker container

4. How I installed and ran Caddy:

Using docker with the following image:

  • caddy:latest

a. System environment:

  • OS: Ubuntu 24.04
  • Docker Version: `Docker version 27.3.1, build ce12230``

b. Command:

docker compose up

c. docker-compose.yml:

services:
  caddy:
    image: caddy:latest
    container_name: caddy
    ports:
      - 8080:80
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile

  whoami:
    image: jwilder/whoami
    deploy:
      replicas: 3

d. Caddyfile:

  • With standard reverse proxy
:80 {
    reverse_proxy whoami:8000
}
  • With dynamic upstream reverse proxy
:80 {
    reverse_proxy {
      lb_policy round_robin
      dynamic a {
        name whoami
        port 8000
        refresh 1s
      }
    }
}

5. Links to relevant resources:

Another post with a related question: Loadbalance docker compose replicas

I don’t think Caddy caches any DNS. Actually, I don’t think Caddy code specifically touches DNS at all (except for dynamic upstreams); I think we lean on net/http for that, and if I remember rightly that ends up with one DNS request per outgoing HTTP request since net/http doesn’t maintain that kind of state. Even dynamic upstreams can’t really be said to “cache DNS” as it’s not saving a response so much as it’s refreshing DNS every refresh-period and building a list of upstreams from it; those results aren’t relevant to future DNS requests.

Your local resolver might cache DNS responses, though.

As for Caddy reliably connecting to the first IP address - I think that’s just a side effect of it being the first IP address returned to Caddy.

It’s interesting to note nginx’s behaviour here. Personally, it seems like one entirely valid interpretation of a single upstream that resolves to multiple IPs is that it’s a single host with redundant routes. For example, in my network, all my Windows servers (DCs and app/file hosts) are connected to two switch ports and have two IPs, so load balancing really doesn’t make all that much sense except maybe from a network throughput perspective.

So, to me, it kinda makes sense to see Caddy treat a single upstream as a single upstream. When you use dynamic upstreams, you’re ruling that possibility out.

1 Like

Hello, @Whitestrake!

Thank you very much for your reply.

The use case I am trying to solve is the one I comment in the post. When having a Docker service scaled across multiple instances, how do I configure Caddy Server to balance between those instances? My initial perception was that, with no configuration beyond reverse_proxy whoami:8000, balancing was already going to happen. Why? Because every time Caddy tried to invoke the upstream service it was going to get the DNS records for that domain and, by default, Docker would balance them with a Round Robin strategy. Something similar to what Nginx does.

Since the above solution does not work, I understand that this is why it is necessary and appropriate to use dynamic upstreams. Am I right?

Thank you very much again.

Best regards.

Yeah, I’m pretty sure dynamic upstream is the way to go to get load balancing.

The way you’ve configured it is pretty spot-on. Personally maybe I’d leave the refresh a little longer just to reduce the total amount of DNS talk, and passive health checks to ensure Caddy can detect and avoid an upstream that has been removed since the last DNS check until the next one takes place. But that’s just preference, I see no reason the way you proposed wouldn’t work perfectly fine.

The alternative if you use lucaslorentz/caddy-docker-proxy is to take advantage of the templated {{upstreams}} which will dynamically generate and reload Caddy’s configured upstreams in essentially real time based off information from the Docker socket.

1 Like

Hello, @Whitestrake.

Great, it is clear to me what you are saying and I agree with you. For the moment I will stick with the alternative of dynamic upstreams, since I don’t want to use CDP (although it also seems to me a good alternative). The refresh rate was set this way to exaggerate the configuration and to make the balancing evident, but obviously it is necessary to adjust it better :slight_smile:

Again, thank you very much for your answer.

Best regards!

1 Like