Port forwarding into container on overlay network with docker swarm does not work

Hello,

I have two hosts (two VMs), where each host will run two containers each, one app and one database (YugabyteDB). The containers will be deployed with two different docker compose files. The two hosts have IPs 193.168.120.243 and 193.168.120.244.

The two different database containers will be connected using an overlay network created by docker swarm init. I have no plan to create swarm services. My plan is only to enable communication between the database containers on the overlay network.

Hence, I have the following networks:

  • external_network: this is the overlay network between the two database containers deployed on the two hosts. This ends up on a 10.0.1.0/24 subnet.
  • internal_network: this is a standard bridge network between the app and database containers. This ends up on a 172.19.0.0/16 subnet.

I have managed to create the swarm cluster and I can successfully communicate between the database containers on the overlay network. When inspecting the database containers, I can see that they are part of both the external 10.0.1.0/24 subnet and the internal 172.19.0.0/16 subnet. The IP assigned to the database container on the external network is 10.0.1.2 and on the internal network 172.19.0.2.

So far so good.

Now to the problem:
It seems that the overlay network refuses any external connection to it. There are two connections to the database containers I need:

  • I want to be able to access the database container (running a YubabyteDB UI) on port 15433 and also two special dashboards available on ports 7000 and 9000 from each of the hosts. For example: 193.168.120.243 → 10.0.1.2 on port 15433.
  • Each app container needs to communicate with the database containers on ports 5433. For example: 172.19.0.3 → 172.19.0.2 on port 5433.

Both of these connections are refused and I cannot understand why. All ports needed are forwarded as required in the docker compose file. With every attempt, the access into the overlay network from either the host or another container seems impossible. Only access from nodes within the overlay network has been successful.

For example, this is how it looks when I try to reach the database UI from the host on port 15433:

curl -v 192.168.120.243:15433
*   Trying 192.168.120.243:15433...
* connect to 192.168.120.243 port 15433 failed: Connection refused
* Failed to connect to 192.168.120.243 port 15433: Connection refused
* Closing connection 0
curl: (7) Failed to connect to 192.168.120.243 port 15433: Connection refused

This is the message get when trying to reach the database on port 5433 from the app container:

ERROR    | app.database.session:<module>:36 - Failed attempt to connect to the database: (psycopg2.OperationalError) connection to server at "db" (172.19.0.2), port 5433 failed: Connection refused

However, if I log into the running database container and run curl -v 10.0.1.2:15433, it can access the UI with no issues. Hence, the issue is only present from outside the overlay network. The port forwarding in the docker compose file does not seem to work.

If we only look from the perspective of host with IP 193.168.120.243. Here is the compose file:

name: my_project

networks:
                  
  internal_network:
    name: internal_network
    driver: bridge

  external_network:
    name: external_network
    driver: overlay
    attachable: true

services:

  app:
    image: app-image:1.0
    container_name: my_app
    hostname: host_name
    networks:
      - internal_network
    restart: always
    environment:
      DATABASE_URL: postgresql://my_user:123my_password@db:5433/my_db
    ports:
      - 80:8000
    depends_on:
      - db

  db:
    image: yugabytedb/yugabyte:2025.1.0.1-b3
    container_name: my_container_name_1
    hostname: my_hostname_1
    networks:
      - internal_network
      - external_network
    restart: always
    command: [ "bin/yugabyted",
               "start",
               "--background=false",
               "--advertise_address=my_container_name_1",
               "--cloud_location=my_cloud.my_region_1.my_zone_1" ]
    environment:
      POSTGRES_DB: my_db
      POSTGRES_USER: my_user
      POSTGRES_PASSWORD: 123my_password
    ports:
      - 7000:7000
      - 7100:7100
      - 9000:9000
      - 9100:9100
      - 15433:15433
      - 5433:5433
      - 9042:9042

My questions:

  • Why can I not access the overlay network from outside the overlay network?
  • Should not the ports section in the compose file open them up from both the host as well as other containers?
  • How can I fix this?

I am running v2.39.4 of docker compose.

Can you rephrase that question? I am not sure, if I got you right, but I still try to share some information.

You can connect a standalone container to an overlay network, if it’s defined with attachable: true (like you already use). A container should be able reach every container it shares at least one container network with - preferably by the service name, not the ip, as the ip can change any time.

The port section is used to publish container ports on host ports, it has nothing todo with container to container communication. There are no firewalls, no port filters.

I would suggest starting to test whether your overlay network actually works.

Those are the usual suspects when the overlay network is not working:

  • Firewall needs following ports to be open on all nodes:
    • Port 2377 TCP for communication with and between manager nodes
    • Port 7946 TCP/UDP for overlay network node discovery
    • Port 4789 UDP (configurable) for overlay network traffic
  • The mtu size is not identical on all nodes
    • ip addr show scope global | grep mtu
  • The nodes don’t share a low latency network connection
  • Nodes are running in vms on VMware vSphere with NSX
    • Outgoing traffic to port 4789 UDP is silently dropped as it conflicts with VMware NSX’s communication port for VXLAN
    • Re-create the swarm with a different data-port:
      • docker swarm init --data-path-port=7789
  • Problems with checksum offloading
    • Disable checksum offloading for the network interface (eth0 is a placeholder):
    • ethtool -K eth0 tx-checksum-ip-generic off
1 Like

One more usual suspect: the Docker overlay network MTU is not set correctly, it’s larger than the underlying network.

This might happen when using VLAN between nodes, which usually has a smaller MTU.

It’s harder to diagnose, as a regular ping usually works, so try ping with payload size 1600.

1 Like

Hi @meyay and @bluepuma77 ,

Thank you both for your replies.

I forgot to mention that the firewalls have been deactivated from the start, so there should be no blocking.

I looked at the mtu size and it was 1500 on all global scopes:

ip addr show scope global | grep mtu
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
3705: docker_gwbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
3707: veth446a58c@if3706: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP group default
3708: br-a7d40d74b811: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
3716: veth5b8ef61@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-a7d40d74b811 state UP group default
9973: vethb577164@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-a7d40d74b811 state UP group default

The only result with a non-size of 1500 is this one, but it is not global scope:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever

It is a low latency network. I also tried recreating the swarm with data path port 7789, same result.

We can simplify the issue down to this:

We can ignore all containers except one, the database container, running on the host with IP 192.168.120.243. Lets say we start from scratch, take down all containers, and leave the swarm. Then we do the following:

  • Start the swarm with docker swarm init
  • Start only the db service with docker compose up -d db

I.e. we ignore host 192.168.120.244 and focus only on the first host. The swarm is initialized and the overlay network is created. The database container is started (nothing else). It is part of the 172.19.0.0/16 subnet and the 10.0.1.0/24 subnet. The container runs the standard YubabyteDB image, which provides three UI dashboards, one at port 7000, one at port 9000 and one at port 15433. Here are some observations:

  • The host cannot reach the dashboards at the mentioned ports. I get “Connection refused” when running e.g. curl -v 192.168.120.243:7000.
  • If I log into the container and access the dashboard, it works fine. Tested with e.g. docker exec -it my_container_name_1 curl -v 10.0.1.2:7000.
  • If I would skip the step of creating a swarm, and only deploy the database container, I can access the dashboard fine from the host with e.g. curl -v 192.168.120.243:7000.

I cannot understand why the port forwarding does not apply when the container is placed on the overlay network. As you can see, I have added all ports needed to the compose specification of the db service.

What have I missed?

I created a simpler example to see if there is a problem with published ports on overlay networks:

services:
  web-test:
    image: nginx
    networks:
      overlay-test: {}
    restart: unless-stopped
    ports:
      - 10080:80

networks:
  overlay-test:
    driver: overlay
    attachable: true

So when I execute docker compose up -d, followed by curl -v <host-lan-ip>:10080 I get the response. This establishes that it’s not related to the overlay network itself.

It is most likely be related to the image you use, the command you use to override the image’s default command, and/or the way how the application inside the container handles being in two networks.

Note: service discovery not just works for the service name, container name, host name, but also if you append .<network name> to it, to reach it over the ip used in that network.

Hi @meyay ,

You are completely correct. I did the same test as you and can confirm it does not affect the nginx container. Hence, this issue seems to be Yugabyte related. I have posted this question on the Yugabyte forum to see if anyone knows what is going on.

Note: service discovery not just works for the service name, container name, host name, but also if you append .<network name> to it, to reach it over the ip used in that network.

This is very helpful to know, thanks! I tested this a little and discovered that I could not have underscores (“_”) in the network name to get it to work. Just a warning for anyone who tries it out.

If a swap out the names

  • external_network → external
  • internal_network → internal

and set --advertise_address=my_container_name_1.internal, then I can actually connect to the Yugabyte container from the host. Only problem is that I can no longer connect to this swarm node from my second swarm node. It seems that by changing the advertise address to the internal network, connectivity on the external overlay network is lost.

Underscores are indeed not allowed for dns names. Never were.

It almost sounds like the server binds its port to the ip of the advertise_address, rather than listening on 0.0.0.0. Did you try to run netstat inside the container to check if it’s the case?

Note: if netstat is not available in the Yugabyte container you can make a tool container join the network namespace of the Yugabyte container like this and run the command there :

docker run -it --rm --net container:<yugabyte container name as shown in docker ps> nicolaka/netshoot`

Hi @meyay ,

I understand. I should also say that my real container’s name is not “my_container_name_1”, as that would probably not have worked either in my example above. That was only a name I chose for my example in the forum post.

So to summarize: If I set --advertise_address=containername1.internal, I am able to reach the yugabyte container from the app container on port 5433, running on the same host. The app container lives on the internal network, so this was expected. I am also able to reach the yugabyte UI from the host on e.g. port 7000. However, after I have joined the node on the other host to the swarm, it seems I cannot make a connection between the nodes on the overlay network. In fact, the yugabyte node on the other host starts, fails to make a connection and then shuts down:

Fetching configs from join IP...
ERROR: Node at the join ip provided is not reachable.

Netstat is available in the Yugabyte image. If I run netstat on the running yugabyte node, I just see this:

[root@hostdb1 yugabyte]# netstat
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 hostdb1:jetdirect       hostdb1:40755           ESTABLISHED
tcp        0      0 hostdb1:jetdirect       hostdb1:35311           ESTABLISHED
tcp        0      0 hostdb1:32861           hostdb1:xfs             ESTABLISHED
tcp        0      0 hostdb1:jetdirect       hostdb1:44191           ESTABLISHED
tcp        0      0 hostdb1:34673           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:45765           ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:37415           ESTABLISHED
tcp        0      0 hostdb1:jetdirect       hostdb1:38057           ESTABLISHED
tcp        0      0 hostdb1:37415           hostdb1:xfs             ESTABLISHED
tcp        0      0 hostdb1:46351           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:44937           hostdb1:xfs             ESTABLISHED
tcp        0      0 hostdb1:41471           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:34829           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:jetdirect       hostdb1:59060           ESTABLISHED
tcp        0      0 hostdb1:59060           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:46221           hostdb1:xfs             ESTABLISHED
tcp        0      0 hostdb1:jetdirect       hostdb1:41471           ESTABLISHED
tcp        0      0 hostdb1:jetdirect       hostdb1:45207           TIME_WAIT
tcp        0      0 hostdb1:jetdirect       hostdb1:42567           ESTABLISHED
tcp        0      0 hostdb1:37571           hostdb1:xfs             ESTABLISHED
tcp        0      0 hostdb1:43769           hostdb1:xfs             ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:45855           ESTABLISHED
tcp        0      0 hostdb1:jetdirect       hostdb1:46351           ESTABLISHED
tcp        0      0 hostdb1:45855           hostdb1:xfs             ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:37571           ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:40679           TIME_WAIT
tcp        0      0 hostdb1:47726           66.8.117.34.bc.go:https ESTABLISHED
tcp        0      0 hostdb1:42567           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:43769           ESTABLISHED
tcp        0      0 hostdb1:jetdirect       hostdb1:59048           ESTABLISHED
tcp        0      0 hostdb1:44191           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:59048           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:pyrrho          ods-app.internal:48566  ESTABLISHED
tcp        0      0 hostdb1:43459           hostdb1:xfs             ESTABLISHED
tcp        0      0 hostdb1:41385           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:46221           ESTABLISHED
tcp        0      0 hostdb1:46727           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:jetdirect       hostdb1:40151           ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:41735           ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:32861           ESTABLISHED
tcp        0      0 hostdb1:38057           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:36513           ESTABLISHED
tcp        0      0 hostdb1:36307           hostdb1:xfs             ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:37131           ESTABLISHED
tcp        0      0 hostdb1:cslistener      hostdb1:42292           TIME_WAIT
tcp        0      0 hostdb1:35311           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:40151           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:47962           66.8.117.34.bc.go:https ESTABLISHED
tcp        0      0 hostdb1:36513           hostdb1:xfs             ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:43459           ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:44999           ESTABLISHED
tcp        0      0 hostdb1:41735           hostdb1:xfs             ESTABLISHED
tcp        0      0 hostdb1:jetdirect       hostdb1:34673           ESTABLISHED
tcp        0      0 hostdb1:44999           hostdb1:xfs             ESTABLISHED
tcp        0      0 hostdb1:jetdirect       hostdb1:34829           ESTABLISHED
tcp        0      0 hostdb1:45765           hostdb1:xfs             ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:45939           TIME_WAIT
tcp        0      0 hostdb1:afs3-fileserver ProdMGMTWin01.sam:49339 TIME_WAIT
tcp        0      0 hostdb1:37413           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:40755           hostdb1:jetdirect       ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:44937           ESTABLISHED
tcp        0      0 hostdb1:jetdirect       hostdb1:46727           ESTABLISHED
tcp        0      0 hostdb1:xfs             hostdb1:36307           ESTABLISHED
tcp        0      0 hostdb1:jetdirect       hostdb1:37413           ESTABLISHED
tcp        0      0 hostdb1:jetdirect       hostdb1:41385           ESTABLISHED
tcp        0      0 hostdb1:47786           hostdb1:jetdirect       TIME_WAIT
tcp        0      0 hostdb1:37131           hostdb1:xfs             ESTABLISHED
Active UNIX domain sockets (w/o servers)
Proto RefCnt Flags       Type       State         I-Node   Path
Active Bluetooth connections (w/o servers)
Proto  Destination       Source            State         PSM DCID   SCID      IMTU    OMTU Security
Proto  Destination       Source            State     Channel

I am not sure what can be read from this, but at least there is no visible reference to the overlay network.

Since netstat seem to exist in the container, please execute docker exec -ti -u 0 hostdb1 netstat -tulpn on your docker host and share the output.

I needed to change hostdb1 to my actual container name, but here it is:

docker exec -ti  -u 0 ods-db01 netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.11:45249        0.0.0.0:*               LISTEN      -
tcp        0      0 172.19.0.2:13000        0.0.0.0:*               LISTEN      170/postgres: YSQL
tcp        0      0 172.19.0.2:9042         0.0.0.0:*               LISTEN      16/yb-tserver
tcp        0      0 172.19.0.2:7000         0.0.0.0:*               LISTEN      15/yb-master
tcp        0      0 172.19.0.2:9000         0.0.0.0:*               LISTEN      16/yb-tserver
tcp        0      0 172.19.0.2:9100         0.0.0.0:*               LISTEN      16/yb-tserver
tcp        0      0 172.19.0.2:7100         0.0.0.0:*               LISTEN      15/yb-master
tcp        0      0 172.19.0.2:12000        0.0.0.0:*               LISTEN      16/yb-tserver
tcp        0      0 172.19.0.2:15433        0.0.0.0:*               LISTEN      65/yugabyted-ui
tcp        0      0 172.19.0.2:5433         0.0.0.0:*               LISTEN      166/postgres
udp        0      0 127.0.0.11:35528        0.0.0.0:*                           -

The open ports seem to be open only on the internal bridged network (172.19.0.0/16) and not the external overlay network (10.0.1.0/24). This came about as I changed the advertise_address to containername1.internal. Before this change, I had only containername1, but that must have defaulted to containername1.external.

  • I need port 5433 to be open on the internal bridged network as the app container needs to access the yugabyte/postgres database.
  • It seems that the host can only reach the three yugabyte UIs (on ports 7000, 9000 and 15433, respectively) when they are open on the internal bridged network. Hence I need these ports open on the internal network as well.
  • The second host on the swarm cluster has its own database node, and that container needs all the ports open on the external overlay network in order to communicate.

What can be done here? It seems that advertise_address locks the port forwarding to one specific network, either internal or external. I need the port forwarding on both networks.

For curiosity, I also tested --advertise_address=0.0.0.0. Then I get the following:

docker exec -ti  -u 0 ods-db01 netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:13000         0.0.0.0:*               LISTEN      198/postgres: YSQL
tcp        0      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      17/yb-tserver
tcp        0      0 127.0.0.1:9042          0.0.0.0:*               LISTEN      17/yb-tserver
tcp        0      0 127.0.0.1:7000          0.0.0.0:*               LISTEN      16/yb-master
tcp        0      0 127.0.0.1:7100          0.0.0.0:*               LISTEN      16/yb-master
tcp        0      0 127.0.0.1:9100          0.0.0.0:*               LISTEN      17/yb-tserver
tcp        0      0 127.0.0.11:41997        0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:12000         0.0.0.0:*               LISTEN      17/yb-tserver
tcp        0      0 127.0.0.1:15433         0.0.0.0:*               LISTEN      58/yugabyted-ui
tcp        0      0 127.0.0.1:5433          0.0.0.0:*               LISTEN      194/postgres
udp        0      0 127.0.0.11:52690        0.0.0.0:*                           -

This result is expected.

With the 0.0.0.0 advertise_address, tests show that neither network (internal or external) can reach the forwarded ports.

What you experience is caused by the applications behavior to bind itself to a specific ip.
There is nothing we can do on the docker side to remedy this.

None of this is related to open ports on the bridge or overlay network. LIke I already wrote: there is no firewall in between, every container can access every other container in the same network, using the ip it has in this network.

Your problem is, that the application inside the container binds to the ip of a specific network, and as such is not reachable by the ip the container has in the other networks.

Neither the problem, nor the solution are on the container level. You need to find a solution within the yugabyte community. Since this is a bug from your perspective, why not raise an issue in their Github repository?

You need to find out how to make the yugabyte processes bind ip 0.0.0.0

Looks like this blog post could be relevant to your situation:

1 Like

I have been using yugabyted, which starts YB-Master and YB-TServer at the same time, on the same container. However, maybe starting two separate containers (master and tserver) give more options and configuration, such as the ones in the article (even if it is for a quite old version of YugabyteDB). I will definitely read it and give it a go. Thanks!

Generally I treat the use of port forwarding to a container an anti-pattern unless:

  • you have a specific service that can only be single replica that’s configured to stop-first.
  • it’s global

The problem with doing something like that would be what you are experiencing where there are port conflicts. Same with Docker Swarm - Zero Downtime

What I would recommend you do is instead plop in a reverse proxy for your dashboards and database UI which I am presuming to be web based.

I would recommend adding a simple Caddy image with a config to do the reverse proxy and mark it as global and you can expose it to a different port from your main web app server.

Something like


services:
  ...
  dashboards:
    image: caddy
    configs:
      - source: dashboards-caddyfile
        target: /etc/caddy/Caddyfile
    volumes:
      - dashboards-caddy-data:/data
      - dashboards-caddy-config:/config
    deploy:
      mode: global
      resources: # Limit the resources no need to let it go wild.
        reservations:
          cpus: "1.0"
          memory: 256M
        limits:
          cpus: "1.0"
          memory: 256M
    ports:
      - target: 80
        published: 12345 # assuming no HTTPS
        mode: host # if you want the real IP sent
    cap_add:
      - NET_BIND_SERVICE
  ...
configs:
  dashboards-caddyfile:
     ...
volumes:
  dashboards-caddy-data:
     ... # can be an EFS/NFS mount
  dashboards-caddy-config:
     ... # can be an EFS/NFS mount

The Caddyfile would look something like (see reverse_proxy (Caddyfile directive) — Caddy Documentation for reference)

:80 {
  reverse_proxy /yubi/* yubi:15443
  reverse_proxy /dashboard1/* dashboard1:7000
  reverse_proxy /dashboard2/* dashboard2:9000
}

Note this also assumes that your backends have the notion of a PREFIX otherwise you’d have to create additional ports for each.

1 Like

I agree with the concept to only have proxies that expose the ports externally.

You can use Traefik as reverse proxy, which can auto-configure Swarm services, check simple Traefik Swarm example.