RMIT University

COSC2767 · Systems Deployment and Operations

Week 9: Docker Compose and Docker Swarm Lab Guide

Define multi-container applications with Compose and orchestrate replicated services with Docker Swarm.

🎯 Objective

Week tutorial screenshot from page 1

In this lab, we will learn the concepts, basic commands and practice these below topics in real applications:

  • Docker Compose: an excellent tool for development, testing, CI workflows, and staging environments.

  • Docker Swarm: a tool allows you to manage Docker engine clusters locally within the Docker platform. You can use the Docker CLI to create a cluster, deploy application services to the cluster, and manage cluster behavior.

1. Docker Compose

Screenshot illustrating 1. Docker Compose

In this section, we will learn how to use Docker Compose to deploy a simple voting application .

A great sample application for docker & docker compose demonstration!

The source files of this voting application is available on Github:

https://github.com/TomHuynhSG/simple-voting-application

Screenshot illustrating https://github.com/TomHuynhSG/simple-voting-application
Screenshot illustrating https://github.com/TomHuynhSG/simple-voting-application

Indeed, this is a distributed application running across five Docker containers.

Screenshot illustrating Indeed, this is a distributed application running across five Docker containers.

The Simple Voting Application Docker project consists of five Docker containers connected as follows:

  • Vote : A Flask front-end app that allows users to vote between two options.

  • Redis : A Redis queue that collects new votes.

  • Worker : A .NET Core and Java application that processes votes and stores them in a PostgreSQL database.

  • Database (db) : A PostgreSQL database container.

  • Result : A Node.js app that displays the voting results in real time.

Docker Compose is used to define and run these multi-container Docker applications. The docker-compose.yml file specifies the configuration of each service and their interconnections.

1a. Create the Docker Compose EC2 server

In this experiment, we only need one single EC2 instance to learn docker-compose:

Screenshot illustrating In this experiment, we only need one single EC2 instance to learn docker-compose:
  • Here are the settings of the EC2:

    • Name : up to you! You can choose the name “voting-server”.

    • OS : default Amazon Linux 2023 AMI (Free-tier)

    • Instance Type : t3.micro (Free-tier)

    • Key pair : select and reuse your old key in other labs (devops_project_key)

    • Storage : Default 8 GB is plenty enough!

    • Security Group:

      • Optional Name: voting-app-security-group

      • Source Type: Anywhere

      • Open these ports:

        • 22 for SSH

        • 8080-8081 (only two ports 8080 and 8081)

1b. Install the required tools

  • After that, let’s SSH to the EC2 server.

Screenshot illustrating After that, let’s SSH to the EC2 server.
  • To keep things simple without dealing with permission, let’s login as a root user and change our current directory to /root:

sudo su -
Screenshot illustrating sudo su -
  • Let's change the hostname of each EC2 server so next time, when we login into that EC2 server, there will be no confusion about which server we are currently login to.

    • It is recommended to change the name of the hostname to be the same as your ec2 server name. In my case, it is “voting-server”.

  • Reboot the server and login with SSH again.

  • Don’t forget to login as the root user:

sudo su -
  • Install Git:

yum install -y git
  • Install Docker:

yum install -y docker
  • Start Docker:

service docker start
  • Check Docker’s service status:

service docker status
Screenshot illustrating service docker status
  • Git clone this repository:

git clone https://github.com/TomHuynhSG/simple-voting-application.git
  • Change current directory inside it:

cd simple-voting-application
  • Let’s list all files and directories inside our voting project:

ls -la
Screenshot illustrating ls -la

1c. Deploy the application without Docker Compose

  • Change current directory to vote directory which contains the voting website:

cd vote

Please feel free to explore this folder to understand how the vote website works.

Screenshot illustrating Please feel free to explore this folder to understand how the vote website works.
  • Checkout the Dockerfile of that voting website:

cat Dockerfile

Can you figure out line by line what it does?

  • Build a Docker image of voting application with that Dockerfile:

docker build . -t voting-app
  • Check if the new voting-app image is there:

docker images
Screenshot illustrating docker images

Aha, the voting-app image is here!

Screenshot illustrating Aha, the voting-app image is here!
  • Run a Docker container from the voting app image ➜ The voting website is now live via port 8080:

docker run --name voting-container -p 8080:80 voting-app
Screenshot illustrating docker run --name voting-container -p 8080:80 voting-app

Remember, we run the container without the “-d” detach mode so we can see what is happening inside the container when we send the requests via the browser.

Let’s open the website via the port 8080 on your browser:

Screenshot illustrating Let’s open the website via the port 8080 on your browser:

Very nice! However, when you interact with the website by voting for either cats or dogs, we receive an internal server error:

Screenshot illustrating an internal server error:

[Question] Why is that?

If you check your terminal, you will see an error message inside our voting-app container:

Screenshot illustrating If you check your terminal, you will see an error message inside our voting-app container:

[Explanation] In the app.py, there is a line where python flask is looking for the “redis” (in-memory data structure store) to store the new voting. Since there is no redis container running, that’s why we got the error.

Screenshot illustrating got the error.

That’s why we need to run the redis container first!

  • You can stop the running voting-app container by pressing the keys combination “Ctrl+C”

Screenshot illustrating You can stop the running voting-app container by pressing the keys combination “Ctrl+C”
  • Let’s remove the voting-app container so we can start from scratch but correctly this time:

docker ps -a
Screenshot illustrating docker ps -a
docker rm <container_id or container_name>
Screenshot illustrating docker rm <container_id or container_name>
Screenshot illustrating docker rm <container_id or container_name>

Good! Let’s start from scratch again!

Screenshot illustrating Good! Let’s start from scratch again!
  • Run a Docker container from the Redis image of DockerHub:

docker run -d --name=redis redis

[Info] You can learn more about the redis image on Docker Hub here: https://hub.docker.com/_/redis. Keep in mind that in a voting application (like polls, upvotes, likes), Redis is often used as a message queue or in-memory store to handle a high volume of votes quickly. Redis is an in-memory database, so it can handle thousands or millions of vote operations per second with very low latency. The purpose of a Redis container in a voting queue system is to act as a fast, reliable middle layer that collects votes instantly , ensures they are processed safely, and relieves pressure on the backend database.

  • Check if the new redis container is there and running:

docker ps
Screenshot illustrating docker ps
  • Run a Docker container from the voting app image ➜ The voting website is now live via port 8080:

docker run -d --name voting-container -p 8080:80 --link redis:redis voting-app
Screenshot illustrating docker run -d --name voting-container -p 8080:80 --link redis:redis voting-app

Notice that, we use -d detach mode so a Docker container runs in the background of your terminal. It does not receive input or display output. Also, we use --link to add links to another container so they can connect together (allow containers to discover each other and securely transfer information about one container to another container).

So far, we have two containers running: voting-container and redis like below diagram:

Screenshot illustrating So far, we have two containers running: voting-container and redis like below diagram:

Let’s open the website via the port 8080 on your browser:

Screenshot illustrating Let’s open the website via the port 8080 on your browser:

Very nice! Now you can interact with the website by voting for either cats or dogs.

However, we need to complete other containers, so we can have a result page to show the voting result so far.

Screenshot illustrating result so far.
  • Run a Docker container from the Postgres image of DockerHub:

docker run -d --name=db -e POSTGRES_PASSWORD=postgres postgres:9.4

For the Postgres container, the only variable required is POSTGRES_PASSWORD. The only variable required is POSTGRES_PASSWORDThis environment variable sets the superuser password for PostgreSQL. The default superuser is defined by the POSTGRES_USER environment variable. You can read it more here: https://hub.docker.com/_/postgres

  • Check if the new postgres container is there and running:

docker ps
Screenshot illustrating docker ps

So far, we have three containers running: voting-container, db and redis like below diagram:

Screenshot illustrating So far, we have three containers running: voting-container, db and redis like below diagram:
  • Change current directory to worker-app directory:

cd ../worker
  • Checkout the Dockerfile of that voting website:

cat Dockerfile

Can you figure out line by line what it does? Note : this Dockerfile is building and packaging a .NET Core Worker service to read from the Redis votes list, aggregates votes, and writes results eventually in PostgreSQL containers.

  • Build a Docker image of worker app with that Dockerfile:

docker build . -t worker-app
  • Check if the new work image is there:

docker images
Screenshot illustrating docker images
  • If you look inside “Worker.java” in “src/main/java/worker” directory, you should see that the Worker application depends on redis and db services:

Screenshot illustrating Worker application depends on redis and db services:
Screenshot illustrating Worker application depends on redis and db services:
  • Run a Docker container from the worker app image. Note, do you notice we have “--link redis:redis --link db:db” to link connections between different containers?:

docker run -d --name worker-container --link redis:redis --link db:db worker-app
  • Check if the new worker container is there and running:

docker ps
Screenshot illustrating docker ps
  • Change current directory to result directory which contains the result website:

cd ../result
  • Checkout the Dockerfile of that result website:

cat Dockerfile

Can you figure out line by line what it does?

Note : this Dockerfile is building the Result website of the voting app with Node.js. This Result app reads the votes from PostgresSQL container and serves a web page with current vote counts.

  • Build a Docker image of result app with that Dockerfile:

docker build . -t result-app
  • Check if the new result-app image is there:

docker images
Screenshot illustrating docker images
  • Run a Docker container from the result app image ➜ The result website is now live via port 8081:

docker run -d --name result-container -p 8081:80 --link db:db result-app
Screenshot illustrating docker run -d --name result-container -p 8081:80 --link db:db result-app
  • Check if all five containers are running:

docker ps
Screenshot illustrating docker ps

Now, you can open the browser to check the result website via the port 8081:

Screenshot illustrating Now, you can open the browser to check the result website via the port 8081:
Screenshot illustrating http://public-ip-address-ec2:8081/):

[Tips for testing] you can test the app by opening the 8080 in incognito mode in the browser to be able vote multiple times and the result website should show the result updated in real time!

Screenshot illustrating able vote multiple times and the result website should show the result updated in real time!

In conclusion, you have successfully deployed the voting application by running and connecting 5 different containers.

Screenshot illustrating different containers.

1d. Deploy the application with Docker Compose

It took some time to deploy this application by running line by line commands.

A better way is to use Docker Compose by defining a YAML file to configure your Docker application's services. Let’s get started!!!

  • Let’s install Docker Compose :

sudo curl -L
https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname
-s)-$(uname -m) -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

You can check out more about the installation of Docker Compose here: https://docs.docker.com/compose/install/linux/

  • Let’s check the version of Docker Compose:

docker-compose version
Screenshot illustrating docker-compose version
  • Let’s start from scratch so we need to stop all running containers and remove all containers:

docker kill $(docker ps -q)
docker rm $(docker ps -a -q)
Screenshot illustrating docker kill $(docker ps -q) docker rm $(docker ps -a -q)
Screenshot illustrating docker kill $(docker ps -q) docker rm $(docker ps -a -q)
  • Let’s list all containers again to make sure all have been stopped and removed:

docker ps -a
Screenshot illustrating docker ps -a

Note: please don’t delete any docker images as we still need to use them for our docker compose exercises.

Nice, now we just need to create a file docker-compose.yml.

  • Before that, let’s get back to the home directory of root user:

cd ~
  • Create a file docker-compose.yml

nano docker-compose.yml

Historically, Docker Compose configuration files evolved from version 1 through 3. However, the Compose Specification (found at https://github.com/compose-spec/compose-spec) has become the standard. Instead of requiring a specific version number, Docker Compose (v2 and later) interprets the file using the latest specification.

https://docs.docker.com/compose/compose-file/compose-file-v3/

Here is the content of our docker.compose.yml. Can you make sense of this file based on the documentation?

  • Note: By default, Docker Compose creates a network for these services. Each service can be reached by its service name (e.g., "vote", "redis", "db").

services:
 redis:
   image: redis
 db:
   image: postgres:9.4
   environment:
    POSTGRES_PASSWORD: "postgres"
 vote:
   image: voting-app
   ports:
    - "8080:80" # Expose the vote service on port 8080 (http://<EC2_IP>:8080)
   depends_on:
    - redis
 worker:
  image: worker-app
  depends_on:
   - db
   - redis
result:
  image: result-app
  ports:
   - "8081:80" # Expose the result service on port 8081 (http://<EC2_IP>:8081)
  depends_on:
   - db

[Explanation] This Compose file sets up a group of connected services. Each service runs in its own container: Redis provides a database cache, Postgres is a database storing data, a " vote " web app is accessible on port 8080, a " worker " service processes background jobs using both Redis and Postgres, and a " result " web app shows the results on port 8081. The depends_on lines ensure some services start before others, and all services can communicate with each other by their names on a shared network.

  • To run the docker-compose.yml, we just need to run the command:

docker-compose up
Screenshot illustrating docker-compose up

Let’s check the website via port 8080 and 8081 ! Everything should work like before! Yayyyyy!

  • To stop our Docker Compose from running then you can press Ctrl + C:

Screenshot illustrating To stop our Docker Compose from running then you can press Ctrl + C:
  • In case, you are interested in running this docker compose in the “detached” mode (With -d, you immediately get back your shell prompt, and the containers keep running in the background), you can try:

docker-compose up -d
Screenshot illustrating docker-compose up -d
  • You can try to inspect the running containers due to that Docker compose:

docker ps
Screenshot illustrating docker ps
  • When you run with -d (detached mode), the process is no longer tied to your terminal, so you can’t just stop it with Ctrl+C. Instead, you manage it through Docker Compose commands:

docker-compose down
Screenshot illustrating docker-compose down

Congratulations for successfully deploying the voting application using Docker Compose! Amazing skills!

2. Docker Swarm

In this section, we will learn how to create a swarm cluster (master and workers)

Screenshot illustrating In this section, we will learn how to create a swarm cluster (master and workers)

2a. Create the Swarm manager and worker servers

In this experiment, we only need three EC2 instance to learn docker swarm:

  • Here are the settings of these three EC2 instances:

    • Name : “docker-master”, “docker-worker-1” or “docker-worker-2

    • OS : default default Amazon Linux 2023 AMI (Free-tier)

    • Instance Type : t3.micro (Free-tier)

    • Key pair : select and reuse your old key in other labs (devops_project_key)

    • Storage : Default 8 GB is plenty enough!

    • Security Group:

      • Optional Name: docker-swarm-security-group

      • Source Type: Anywhere

      • Open these ports:

        • 22 for SSH

        • 2376 : used for secure Docker client communication. This port is required for Docker Machine to work. Docker Machine is used to orchestrate Docker hosts.

        • 2377 : used for communication between the nodes of a Docker Swarm or cluster. It only needs to be opened on manager nodes.

        • 7946 : Used for communication among nodes (container network discovery).

        • 4789 : overlay network traffic (container ingress networking).

        • 8080-8090 for web application

Please create the security group “ docker-swarm-security-group ” once and let other instances reuse the existing security group.

2b. Install and configure Docker

Please choose either Option A - Manual Setup or Option B - Auto Setup with User Data Script:

Option A - Manual Setup:

  • Please SSH login to all of the EC2 instances.

  • Please change the host name for all EC2 instances accordingly so we can easily manage them like so:

Screenshot illustrating like so:
Screenshot illustrating like so:
Screenshot illustrating like so:

Hint : for a reminder, you can use this command to change the hostname:

sudo su -
echo "new-host-name" > /etc/hostname
reboot
  • Don’t forget to login as the root user:

sudo su -
  • Install docker on all EC2 instances:

yum install -y docker
  • Start docker service on all EC2 instances:

service docker start

Option B - User Data Script:

  • Click on Advanced details

Screenshot illustrating Click on Advanced details
  • Upload or paste in the User data:

Screenshot illustrating Upload or paste in the User data:

When you configure the EC2 instances above, please paste in this example EC2 User Data script when launching each EC2 instance. With this script, you can automate setting the hostname, installing Docker, and starting the Docker service. You will need to tailor the variable HOSTNAME to match each instance’s intended hostname (e.g., docker-master , docker-worker-1 , docker-worker-2 ).

  • To use different hostnames for each instance:

    • For the master: Replace HOSTNAME="docker-master"

    • For worker 1: Replace HOSTNAME="docker-worker-1"

    • For worker 2: Replace HOSTNAME="docker-worker-2"

#!/bin/bash
# Set a variable for the hostname. Modify this value as needed.
HOSTNAME="docker-master"
# Set the hostname using the variable
hostnamectl set-hostname "$HOSTNAME"
# Update packages
yum update -y
# Install Docker
yum install -y docker
# Start Docker service
service docker start
# Enable Docker to start on boot
systemctl enable docker

After Option A and Option B, continue below here:

  • Check the status of docker service on all EC2 instances to make sure it is active (running):

service docker status

2c. Practise Docker Swarm commands

  • On the master node , please start the swarm (by selecting it to be swarm master/manager):

docker swarm init
Screenshot illustrating docker swarm init
  • On the slave/worker node , please join the swarm by copying and pasting the above suggested command:

docker swarm join --token <long-token-string>
Screenshot illustrating docker swarm join --token <long-token-string> (1 of 2)Screenshot illustrating docker swarm join --token <long-token-string> (2 of 2)
  • On the master node , you can list all nodes in the swarm.

docker node ls
Screenshot illustrating docker node ls

* means the current server that you are on.

  • A node can leave the swarm by running the command so please switch to one of the workers and make it leave the swarm:

docker swarm leave
Screenshot illustrating docker swarm leave
  • On the master node, you can list all nodes in the swarm again.

docker node ls
Screenshot illustrating docker node ls

As you can see the docker-worker-1 is down and left the swarm already!

  • Let’s add the docker-worker-1 back to the swarm again!

Screenshot illustrating Let’s add the docker-worker-1 back to the swarm again!
  • On the master node, you can list all nodes in the swarm again. Note, you can see that the same node joining again will have a new ID instead of the old one.

docker node ls
Screenshot illustrating docker node ls
  • Let's remove the node which is already down so our node list can be more organised:

docker node rm <node_id or node_hostname>
Screenshot illustrating docker node rm <node_id or node_hostname>

2d. Deploy services on the Swarm

  • On the master node , let's list all service commands of docker:

docker service --help
Screenshot illustrating docker service --help

Let’s see Docker Swarm in action! An NGINX container is a Docker container that runs the NGINX high-performance web server. It is often used as a “hello world” example container because it’s simple, fast, and demonstrates many important container concepts without complexity. Read more here: https://hub.docker.com/_/nginx

In Docker Swarm, a service is the fundamental unit of deployment and scaling. It’s essentially a definition that tells the swarm what container image to run, how many replicas of it to maintain, and how to distribute them across the cluster.

  • On the master node , create a service of nginx web server:

docker service create nginx
Screenshot illustrating docker service create nginx
  • On the master node , we can list services:

docker service ls
Screenshot illustrating docker service ls
  • On the master node , we can list the task of that service by its id:

docker service ps <service-id>
Screenshot illustrating docker service ps <service-id>

Note that: the task is running on the “docker-master" node.

In Docker Swarm, a task is the atomic unit of scheduling in Docker Swarm. Each replica of a service corresponds to one task, and that task runs exactly one container.

👉 So in short:

Service = the definition and desired state (e.g., “run 3 replicas of nginx”).

Task = one running instance of that definition (e.g., one container, on one node).

  • However, we forgot to define which port number to expose for our nginx server, so let's update the service accordingly:

docker service update <service-id> --publish-add 8080:80
Screenshot illustrating docker service update <service-id> --publish-add 8080:80
  • On the master node, we can list services:

    • Note: after updating the port number, the old container on docker-master is shutdown, the updated container is now running on docker-worker-2 in the screenshot (in your case, it could pick any random node in the cluster to run the container).

docker service ps <service-id>
Screenshot illustrating docker service ps <service-id>

Open the public ip address of the ec2 instance where the updated container is on (in our screenshot, it shows it runs on docker-worker-2) and you can check the website on the browser via the port 8080 as below:

Screenshot illustrating the port 8080 as below:
  • On the master node, let's remove that service:

docker service rm <service-id>
Screenshot illustrating docker service rm <service-id>
  • No more service is running so the container should be shutdowned and removed.

Screenshot illustrating No more service is running so the container should be shutdowned and removed.
  • On the master node , let's create a new service with replicas parameters :

    • Replicas in Docker ensure high availability and scalability by running multiple instances of a service, as seen in this command, which creates an Nginx service with 2 replicas , exposing port 8080 to the target port 80 on each instance.

docker service create --name nginx –=--publish published=8080,target=80 nginx
Screenshot illustrating docker service create --name nginx –=--publish published=8080,target=80 nginx
  • Let’s check the information of our new service:

docker service ls
Screenshot illustrating docker service ls

Note : The above screenshot shows that Docker confirms the nginx service with 2 replicas has been created and both containers are running successfully. With the --publish flag you mentioned, the service would also be accessible on port 8080 of the swarm nodes, routing traffic into container port

  • Let’s inspect inside the tasks in our service:

    • You don’t have to use the raw service ID like docker service ps <service-id>. You can also use the service name.

docker service ps <service-name>
Screenshot illustrating docker service ps <service-name>

As you can see, one container is running at docker-worker-2 node and one container is running at docker-master node, showing proper swarm scheduling and service distribution

  • [Switching terminal to where first container runs (eg, docker-worker-2)] You can confirm if the container is actually running in the docker-worker-2 node:

docker ps
Screenshot illustrating docker ps
  • [Switching terminal to where second container runs (eg, docker-master)] You can also confirm if the container is actually running in the docker-master node:

docker ps
Screenshot illustrating docker ps
  • [Switching terminal to docker-master] However, if you do not want the docker-master node to run any container, just to control the swarm to ensure the master node have little of burden then you can run the command on the master node:

docker node update --availability drain docker-master
Screenshot illustrating docker node update --availability drain docker-master
  • After applying our previous command, the container of nginx in the master node is shut down, thus a new container of nginx in docker-worker-1 is launched to ensure the number of containers of nginx in the swarm is always 2 (the effect of replicas).

docker service ps nginx
Screenshot illustrating docker service ps nginx

Note : you should see that by draining docker-master, the container that was running there was stopped, and Swarm automatically launched a replacement container on another worker (docker-worker-1). This demonstrates Docker Swarm’s self-healing and load-balancing behavior: it keeps the service at the desired replica count, even when nodes are marked unavailable.

Good job so far!

Alright, hopefully now the docker-worker 1 and docker-worker 2 are listening to the port 8080 to serve the nginx websites. Please open the public IP address of docker-worker 1 and docker-worker 2 via the port 8080 to test if the homepage of nginx web server is showing. [Our Next Task] However, we want users to access the website via a common IP address or domain name so the incoming requests will be spread out for all of the docker-worker instances.

Screenshot illustrating name so the incoming requests will be spread out for all of the docker-worker instances.

🏆

  • That's why we need to create a load balancer on AWS to do that!

  • Let's go! On the AWS dashboard, select the load balancer:

Screenshot illustrating Let's go! On the AWS dashboard, select the load balancer:
  • Choose Application Load Balancers as we want to redirect traffic from the balancer to our nginx application servers:

Screenshot illustrating nginx application servers:
  • Enter basic information of your balancer:

    • Load balancer name : my-docker-swarm-load-balancer

      • A unique identifier for the load balancer within the AWS account.

    • Scheme: Internet-facing

      • This makes the load balancer accessible from the internet.

      • It gets public IP addresses.

      • Its DNS name is publicly resolvable.

      • Requires mapping to a public subnet.

      • Best when your Docker Swarm service should be accessible from outside (e.g., web apps).

    • IP address type: IPv4

      • Restricts the load balancer to only IPv4 addresses.

      • Suitable if your clients/users connect using IPv4 (most common scenario).

      • Simple and widely supported.

Screenshot illustrating Simple and widely supported.
  • Availability Zone Mappings:

    • You can choose subnets in Availability Zones (AZs) within the VPC where the load balancer will route traffic.

    • Ideally, you should choose the specific availability zones which actually contain your EC2 instances. However, to keep to it simple, we can select all of them: us-east-1a, us-east-1b, us-east-1c, us-east-1d, us-east-1e, us-east-1f

      • High availability: Even if one AZ goes down, traffic can still be routed through others.

      • Better load distribution: The load balancer can spread traffic across multiple zones.

      • Resilience: Minimizes single points of failure.

Screenshot illustrating Resilience: Minimizes single points of failure.
  • We can select the same security groups of our docker security group to keep it simple (in theory, you only need to choose a group with the port you want to serve your clients, in our case, we can keep it simple by also choosing the port 8080):

Screenshot illustrating case, we can keep it simple by also choosing the port 8080):
  • We need to create the target group first before we can route all traffics of balancer to it:

Screenshot illustrating We need to create the target group first before we can route all traffics of balancer to it:
  • Create a target group of EC2 instances:

Screenshot illustrating Create a target group of EC2 instances:
  • Let's enter the name of our target group of EC2 instances with the port 8080:

Screenshot illustrating Let's enter the name of our target group of EC2 instances with the port 8080:
  • Go next page:

Screenshot illustrating Go next page:
  • Add all the docker-worker instances (docker-worker-1 and docker-worker-2) to this group:

Screenshot illustrating Add all the docker-worker instances (docker-worker-1 and docker-worker-2) to this group:
  • Confirm the group details and finish the group registration:

Screenshot illustrating Confirm the group details and finish the group registration:
  • Now, we can go back to the balancer setting page and select the target group we just created:

Screenshot illustrating created:
  • Confirm to create the balancer:

Screenshot illustrating Confirm to create the balancer:
  • View your balancer after creating it:

Screenshot illustrating View your balancer after creating it:
  • Now, you can see the DNS name of the balancer which will be the URL link you should give to your clients/customers to open the website:

    • You should see that the load balancer is now active, internet-facing, and listening on port 8080. You can use the provided DNS name with the port 8080 to access your Docker Swarm Nginx service running across multiple nodes, with AWS ELB automatically distributing the traffic.

Screenshot illustrating automatically distributing the traffic.

Note : it might take around two minutes for the load balancers to warm up enough to start directing the traffic so don’t panic if you don’t see the website right away using the DNS of the load balancer!

  • You can use the DNS Name to access the website running in the Docker Swarm service across multiple nodes being distributed in AWS ELB. Great success! Now you can see the website which is served by one of your many docker-worker instances within the load balancer!

Screenshot illustrating which is served by one of your many docker-worker instances within the load balancer!

Super awesome! Imagine for a very complex and popular website which got hundreds of thousands of views everyday, with a load balancer and hundreds of instances, you can distribute all the incoming traffics from users across among all of instances to make sure the load for each

Screenshot illustrating all the incoming traffics from users across among all of instances to make sure the load for each

instance small enough and if there is any container crashing then a new container will be launched to replace the old crashed one to make sure the availability of the website is always high!

  • You can check the visual resources map to see how load balancer redirect the traffic from the start to the end to ensure user requests to land on either one of the instances in the load balancer:

    • Traffic hitting the DNS of your load balancer on port 8080 → listener → rule → forwarded to target group (docker-swarm) → distributed between 2 healthy EC2 instances (each serving Nginx on port 8080). This confirms that your Docker Swarm Nginx service is correctly integrated with the AWS Load Balancer and is ready for high availability.

Screenshot illustrating high availability.
  • Also, you can manage the health and traffic of your load balancer using CloudWatch metrics in the Monitoring tab as below:

    • The most important metrics to continuously monitor in CloudWatch for ELB are:

      • Target Response Time (latency)

      • Requests Count (traffic volume)

      • Target 4XXs and 5XXs (client/server errors)

      • ELB 5XXs (502/503/504) (load balancer-level issues)

      • Connection Errors / Rejected Connections (connectivity & scaling issues)

    • These will tell you whether your Docker Swarm Nginx service is healthy, responsive, and reachable through the load balancer.

Screenshot illustrating and reachable through the load balancer.

Alright! I hope that's enough for you to have a taste of Docker Compose, Docker Swarm and Load Balancer.

Congratulations on completing this lab! You’ve taken important steps into the world of container orchestration and DevOps practices.

What you’ve built here may be a simple voting application, but the skills you’ve practiced are the same ones used to power large-scale, production-grade systems. Each command and configuration you’ve written is another building block toward mastering containerized deployments.

Remember:

  • Docker Compose helps you quickly set up and manage services for development and testing.

  • Docker Swarm allows you to orchestrate containers across clusters with reliability and scalability.

Screenshot illustrating scalability.

🧪 Challenge: Add Prometheus monitoring with Docker Compose

Goal

Enhance your Docker Compose skills by extending the existing multi-container application to include a new service. You'll configure Prometheus to monitor key metrics of your deployed application.

Scenario and implementation steps

Imagine you’re running the voting application in a real-world production environment, where monitoring application health and performance is critical. You’ve been tasked to integrate Prometheus , a popular metrics monitoring tool, into the existing Docker Compose setup. This new service will track application metrics, such as vote counts, and provide insights into application behavior through real-time monitoring.

Step-by-step guide

  • For these exercises we will get back to work on the voting server.

  • Open the new port 9090 for the security group for Prometheus.

Screenshot illustrating Open the new port 9090 for the security group for Prometheus.
  • SSH to your EC2 voting-server as root again!

sudo su -
  • Ensure your existing application is running by starting by confirming your original application is working correctly:

docker-compose up -d
Screenshot illustrating docker-compose up -d
  • Check that all services (vote, worker, result, redis, db) are healthy:

docker ps
Screenshot illustrating docker ps

This is an example of how you can modify your Dockerfile to integrate Prometheus instrumentation for the Python (Flask) vote application. What we need to do:

  • Install the prometheus_client Python library inside the container so we can use it in the app.py.

  • In app.py, we will add code to expose a /metrics endpoint and increment counters or other metrics. (This part is done in code, not in Dockerfile.)

  • Let’s get started by changing working directory to the vote folder:

cd simple-voting-application/vote
  • Add prometheus library to requirements.txt: if you add prometheus to your requirements file, you don’t need to change much in the Dockerfile. Your requirements.txt might look like this:

Flask
Redis
gunicorn
prometheus_client
  • [Optional] Alternatively, you can instead choose to install prometheus directly in the dockerfile like:

…
RUN pip install -r requirements.txt
# Additional line to install prometheus_client
RUN pip install prometheus_client==0.16.0
…
  • Next, you need modify the existing app.py to integrate Prometheus metrics. We will:

    • Import the Prometheus client library.

    • Define a metric (a Counter) to track votes.

    • Increment the counter each time a vote is cast.

    • Expose a /metrics endpoint to allow Prometheus to scrape these metrics.

... import logging

# Import Prometheus client library from prometheus_client import Counter, generate_latest

option_a = os.getenv('OPTION_A', "Cats")

... app.logger.setLevel(logging.INFO)

# Define a counter metric to count the number of votes per choice vote_counter = Counter('app_votes_total', 'Total number of votes', ['choice'])

def get_redis(): ...

... app.logger.info('Received vote for %s', vote)

# Increment the Prometheus counter for the chosen option vote_counter.labels(choice=vote).inc()

data = json.dumps({'voter_id': voter_id, 'vote': vote}) ...

# Expose a /metrics endpoint for Prometheus scraping
@app.route("/metrics")
def metrics():
   return generate_latest(), 200, {'Content-Type': 'text/plain; charset=utf-8'}
if __name__ == "__main__":
   app.run(host='0.0.0.0', port=80, debug=True, threaded=True)
  • Rebuild your Docker image (after ensuring prometheus_client is installed via requirements.txt or a pip install line in the Dockerfile). Make sure the image can be successfully built.

docker build . -t voting-app
  • Change to the directory where your docker-compose.yml resides (likely /root if you followed the initial instructions).

cd ~
  • Create the prometheus.yml file to scrape the vote app’s /metrics endpoint:

global:
 scrape_interval: 2s
scrape_configs:
 - job_name: 'vote_app'
   static_configs:
    - targets: ['vote:80']
# Add more scrape_configs as needed once you have other services exposing
metrics.

[Explanation]

  • scrape_interval: Prometheus will scrape every 2 seconds, which is fine for testing.

  • targets: ['vote:80'] : This means Prometheus will try to scrape metrics at http://vote:80/metrics. Since vote is the service name defined in docker-compose.yml and all services share the same Docker network, this should work correctly. Your vote service runs on port 80 internally (mapped to 8080 externally), so internally Prometheus can reach it at vote:80.

With these changes, Prometheus will be able to scrape the /metrics endpoint and display real-time voting data.

  • Edit your existing docker-compose.yml to add a new Prometheus service. You can add it at the bottom of the file.

services: redis: image: redis

db: image: postgres:9.4 environment: POSTGRES_PASSWORD: "postgres"

vote: image: voting-app ports:

  • "8080:80" # Expose the vote service on port 8080 (http://<EC2_IP>:8080)

depends_on:

  • redis

worker: image: worker-app depends_on:

  • db

    • redis

result: image: result-app ports:

  • "8081:80" # Expose the result service on port 8081 (http://<EC2_IP>:8081)

depends_on:

  • db

prometheus: image: prom/prometheus:latest volumes: # Mount your prometheus.yml configuration file into the container

  • ./prometheus.yml:/etc/prometheus/prometheus.yml

ports:

  • "9090:9090" # Expose the service on port 9090(http://<EC2_IP>:9090)

depends_on:

- vote
- result
- db
- redis
- worker

[Explanation]

  • Exposure of Prometheus : Prometheus is exposed at 9090:9090 , so you can access the Prometheus UI at http://<EC2_PUBLIC_IP>:9090.

  • Mounting of prometheus.yml: You’ve mounted ./prometheus.yml into /etc/prometheus/prometheus.yml in the Prometheus container.

  • Service Dependencies: You have depends_on entries for each service. While depends_on ensures the order of starting containers, it doesn’t guarantee that a service is fully ready. However, since Prometheus continuously scrapes, it’s not a big issue.

  • Now, we are ready! Let’s start the entire stack with docker compose:

docker-compose up -d
Screenshot illustrating docker-compose up -d
  • Now you can access:

    • Access the vote app at http://<EC2_PUBLIC_IP>: 8080

    • Access the result app at http://<EC2_PUBLIC_IP>: 8081

    • Access Prometheus dashboard at http://<EC2_PUBLIC_IP>: 9090

If everything is configured correctly, Prometheus should start scraping the vote service’s metrics, and you’ll be able to see them in Prometheus under the “Status -> Targets” pages.

Screenshot illustrating and you’ll be able to see them in Prometheus under the “Status -> Targets” pages.
  • Time to test app_votes_total metric output!

    • First, please open the browser incognito mode to allow you to make multiple votes to at least increase the count by one. Prometheus client libraries only start reporting counter metrics after they’ve been incremented at least once

      • In my example, I manually vote many times for testing:

Screenshot illustrating In my example, I manually vote many times for testing:
  • Check the metrics endpoint directly from the terminal

curl http://<EC2_PUBLIC_IP>:8080/metrics | grep app_votes_total

Replace <EC2_PUBLIC_IP> with your actual server’s IP. You should see something like this (could be slightly different for you):

app_votes_total{choice="a"} x.0
app_votes_total{choice="b"} x.0
Screenshot illustrating app_votes_total{choice="a"} x.0 app_votes_total{choice="b"} x.0

If you don’t see any lines for app_votes_total, it means the application hasn’t recorded any valid votes.

  • Query the Metric in Prometheus dashboard:

    • In the Query box, type app_votes_total and press Enter.

    • You should see the graph for app_votes_total for each option (Cat or Dog).

Screenshot illustrating You should see the graph for app_votes_total for each option (Cat or Dog).

[Website Down Simulation] To simulate the vote website going down, you can simply stop or remove the container running the vote service.

  • Stop the Container: With Docker Compose, you can just stop that one service:

docker-compose stop vote
  • What Happens in Prometheus:

    • Prometheus continuously scrapes targets at the configured interval.

    • Once the vote container is stopped/removed, Prometheus will fail to reach http://vote:80/metrics.

    • After one or two failed scrapes (depending on your configuration), Prometheus will mark the vote_app target as “down.”

    • In the Prometheus UI (http://<EC2_PUBLIC_IP>:9090/targets), you’ll see the vote_app job listed as “down”.

Screenshot illustrating vote_app job listed as “down”.

Great job completing this lab! You’ve learned how to deploy a multi-container app with Docker Compose and scale it with Docker Swarm. These tools make managing complex applications simpler and more efficient, whether for development or production. Keep practicing, and you’ll master these essential DevOps skills in no time!

🎮 Bonus: Practise Docker Swarm online

Screenshot illustrating [Bonus] Learning Docker Swarm with Interactive Online Labs from Docker

In case you want to revise and work on your docker swarm skills, please follow this very short fun learning labs for Docker swarm from Docker:

Screenshot illustrating Have fun! Always be learning!

📚 Continue the course