Operations 8 min read

Expose One Port for Many Docker Containers: Reverse Proxy, DNAT & SO_REUSEPORT

Learn how to make multiple Docker containers share a single external port by using reverse‑proxy load balancing with Nginx, configuring DNAT iptables rules, and leveraging the SO_REUSEPORT socket option, complete with step‑by‑step commands and network setup examples.

Liangxu Linux
Liangxu Linux
Liangxu Linux
Expose One Port for Many Docker Containers: Reverse Proxy, DNAT & SO_REUSEPORT

In the modern web era, applications become more powerful and complex, requiring cluster deployment, isolated environments, gray‑release, and dynamic scaling; containerization bridges these needs.

Docker provides Linux container support, and exposing container services to the outside requires port mapping.

Docker offers four port‑exposure syntaxes:

-p                # map container port to a random host port on all addresses
-p hostPort:containerPort   # map to a specific host port
-p ip::containerPort        # map to a random host port on a specific IP
-p ip:hostPort:containerPort # map to a specific host IP and port

When multiple containers provide the same service, you may want to expose only one external port. Three common approaches are described below.

Reverse Proxy

Use a reverse‑proxy such as Nginx or HAProxy to receive traffic on a single port and forward it to backend containers.

Step 1: Create a Docker network

docker network create my-network

Step 2: Run backend containers on the network

docker run -d --network my-network --name container1 -p 8080:80 image1
docker run -d --network my-network --name container2 -p 8080:80 image2
docker run -d --network my-network --name container3 -p 8080:80 image3

Step 3: Run an Nginx load‑balancer container

docker run -d --network my-network -p 8080:80 --name load-balancer nginx

The Nginx container listens on port 8080 and distributes incoming requests to the three backend containers.

DNAT (iptables)

By creating DNAT rules you can forward traffic arriving at the host to specific container IPs, mimicking Kubernetes NodePort behavior.

Start two containers

CONT_PORT=9090

docker run -d --rm --name http_server_foo -e INSTANCE=foo -e PORT=$CONT_PORT http_server
docker run -d --rm --name http_server_bar -e INSTANCE=bar -e PORT=$CONT_PORT http_server

Obtain their IP addresses

CONT_FOO_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' http_server_foo)
CONT_BAR_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' http_server_bar)

Create DNAT rules

FRONT_PORT=80

# Allow forwarding (test only)
sudo iptables -I FORWARD 1 -j ACCEPT

# DNAT for local traffic
sudo iptables -t nat -I OUTPUT 1 -p tcp --dport $FRONT_PORT \
    -m statistic --mode random --probability 1.0 -j DNAT --to-destination $CONT_FOO_IP:$CONT_PORT
sudo iptables -t nat -I OUTPUT 1 -p tcp --dport $FRONT_PORT \
    -m statistic --mode random --probability 0.5 -j DNAT --to-destination $CONT_BAR_IP:$CONT_PORT

# DNAT for external traffic
sudo iptables -t nat -I PREROUTING 1 -p tcp --dport $FRONT_PORT \
    -m statistic --mode random --probability 1.0 -j DNAT --to-destination $CONT_FOO_IP:$CONT_PORT
sudo iptables -t nat -I PREROUTING 1 -p tcp --dport $FRONT_PORT \
    -m statistic --mode random --probability 0.5 -j DNAT --to-destination $CONT_BAR_IP:$CONT_PORT

These rules forward incoming traffic on port 80 to either container, achieving one‑port‑multiple‑container access.

Multiple Services Listening (SO_REUSEPORT)

The socket option SO_REUSEPORT allows several processes to bind to the same port, enabling multiple containers to listen on the identical address.

func main() {
    lc := net.ListenConfig{
        Control: func(network, address string, conn syscall.RawConn) error {
            var operr error
            if err := conn.Control(func(fd uintptr) {
                operr = syscall.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
            }); err != nil {
                return err
            }
            return operr
        },
    }

    ln, err := lc.Listen(context.Background(), "tcp", os.Getenv("HOST")+":"+os.Getenv("PORT"))
    if err != nil {
        panic(err)
    }

    http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
        w.Write([]byte(fmt.Sprintf("Hello from %s
", os.Getenv("INSTANCE"))))
    })

    if err := http.Serve(ln, nil); err != nil {
        panic(err)
    }
}

By building several containers with this code and placing them on the same Docker network, they can all accept connections on the shared port.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

load balancingreverse proxySO_REUSEPORTport mappingDNAT
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.