Skip to main content

Load Balancing Docker Compose Replicas Using Docker Swarm

·664 words·4 mins
Sebastian Scheibe
Author
Sebastian Scheibe
Table of Contents

Introduction
#

Imagine you are working with Docker Compose and you have deployed your Python or Node.js app with it. You might have noticed that these are usually single-threaded, and you need multiple instances of the same app to make use of the tons of CPU your server has.

When dealing with high traffic or CPU-intensive operations, running multiple replicas of your service is crucial to distribute the load and make full use of your server’s resources.

In that case, you might have discovered that you can use the deploy.replicas count configuration attribute in Docker Compose and saw this error:

Attaching to docker-replica-test-node-1, docker-replica-test-node-2
docker-replica-test-node-1  | Server is running at http://localhost:8080/
Error response from daemon: driver failed programming external connectivity on endpoint docker-replica-test-node-2: 
Bind for 0.0.0.0:8080 failed: port is already allocated

Problem
#

The issue occurs because each container is trying to bind to the same port (8080), but Docker Compose cannot allocate the same port to multiple containers. Docker Compose lacks built-in load balancing for service ports, which leads to a conflict when replicas attempt to use the same port.

Here’s an example of a docker-compose.yml file that causes the issue:

version: '3.7'

services:
  node:
    image: node:22
    working_dir: /usr/src/app
    volumes:
      - ./index.js:/usr/src/app/index.js
    ports:
      - "8080:8080"
    command: node index.js
    deploy:
      replicas: 2

Solution: Switching to Docker Swarm
#

To solve this issue, we need to switch from Docker Compose to Docker Swarm, which does provide port load balancing and allows us to create multiple instances (replicas) of containers.

Initialize Docker Swarm
#

At first, let’s initialize Docker Swarm

docker swarm init

Which will produce the following output:

The token provided is only relevant if you plan to add additional worker or manager nodes to this Docker Swarm cluster.

It can be ignored for now.

docker swarm init
Swarm initialized: current node (izw30cwofxm9brukpg6hi2x8s) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-5wpisj9wudh44z2verbi3fq7omczx7qur0b6jb7zmexeg4tb2t-f0vpjofp761d8p0jfg3yxv6wn 192.168.65.3:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

Deploy the Stack
#

Now, deploy the docker-compose.yml using Docker Swarm by running:

docker stack deploy -c docker-compose.yml test-node

Check Deployed Containers
#

After that, you will see that the actual containers do not expose any ports:

docker ps
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS          PORTS                               NAMES
0e8463b14b19   node:22                  "docker-entrypoint.s…"   54 seconds ago   Up 53 seconds                                       test-node_node.2.0ln7zb4v5tf6heqd2o9f025pr
4bc37314a1c7   node:22                  "docker-entrypoint.s…"   54 seconds ago   Up 53 seconds                                       test-node_node.1.tepsgvoapo6byjpww2uptco10

Check Service Ports
#

The service is now exposing the ports and load balances them across replicas:

docker service ls
ID             NAME             MODE         REPLICAS   IMAGE     PORTS
yc4rxda0obwv   test-node_node   replicated   2/2        node:22   *:8080->8080/tcp

Test the Service
#

Now, check if the ports are exposed and working:

curl localhost:8080

Hello, World!

Your service is now correctly load-balanced across multiple replicas.

Docker Stack Helpful Commands
#

Deploy a Stack
#

docker stack deploy -c docker_compose.yml <stack_name>

For example:

docker stack deploy -c docker-compose.yml test-node
Creating network test-node_default
Creating service test-node_node

Delete a Stack
#

docker stack rm <stack_name>

For example:

docker stack rm test-node
Removing service test-node_node
Removing network test-node_default

List All Stacks
#

docker stack ls

For example:

docker stack ls
NAME        SERVICES
test-node   1

List Services in a Stack
#

docker stack ps <stack_name>

For example:

docker stack services test-node
ID             NAME             MODE         REPLICAS   IMAGE     PORTS
rrloc0uy9wet   test-node_node   replicated   2/2        node:22   *:8080->8080/tcp

List Containers in a Stack
#

docker stack ps <stack_name>

For example:

docker stack ps test-node
ID             NAME               IMAGE     NODE                    DESIRED STATE   CURRENT STATE                ERROR     PORTS
l01gp6p6dhgn   test-node_node.1   node:22   linuxkit-d68c0953dede   Running         Running about a minute ago             
xc0280lsexja   test-node_node.2   node:22   linuxkit-d68c0953dede   Running         Running about a minute ago             

Conclusion
#

With Docker Swarm, it is possible to load balance single-threaded applications using the deploy.replicas count, making efficient use of all available CPU resources. While Docker Compose is excellent for local development, Swarm provides the additional functionality required for production environments that need scaling and load balancing across replicas.

If you’re looking for even more advanced orchestration, Kubernetes might be worth exploring.

Resources
#

Docker Swarm Overview

Docker Stack Command Reference Docker Stack Deploy Command Reference