DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

marcelkatz
marcelkatz

Posted on

Node.js and Redis deployed in Docker containers, using Docker Compose - then load-balancing the Node.js servers with Nginx

This article contains two main stages:
(1) Containerizing a Node.js server application and a Redis database instance into two separate Docker containers, using Dockerfile and Docker Compose, and showing how these two applications communicate with each other.

(2) Load-balancing the Node.js server, using a containerized Nginx reverse-proxy.

Let’s start with Stage 1:
(1) Containerizing a Node.js server application and a Redis instance into two separate Docker containers, using Dockerfile and Docker Compose, and showing how these two applications communicate with each other

Starting with a simple Node.js server application (we’ll call it β€œtest-webapp”) that responds to an HTTP GET request by displaying the β€œnumbers of visits”. The numbering scheme below (i.e. (1.1), (1.2), (1.3) etc.), matches the numbering in the diagram below:

Figure 1.a – Schematic diagram of the components
Figure 1.aβ€Š-β€ŠSchematic diagram of the components

In "Figure 1.aβ€Š-β€ŠSchematic diagram of the components" above we have the following components:
(1.1) "Docker Container 1"β€Š-β€Šcontainer running the Node.js server called "test-webapp" that communicates with the browser on the left. Each time we refresh the URL localhost:80 i.e. we send a GETcommand to the Node.js server "test-webapp", the server code increments the number of visits, then saves this value into the Redis database instance that runs on "Docker Container 2", and also displays the value back in the browser.

(1.2) β€œDockerfile” - defines and controls the Node.js server process in β€œDocker Container 1”.

(1.3, 1.3.1, 1.3.2) β€œdocker-compose.yml” – the Docker Compose config file defines and controls both β€œDocker Container 1” and β€œDocker Container 2”. β€œDocker Container 1” runs the Node.js server process β€œtest-webap_p”. β€œ_Docker Container 2” runs the Redis database instance.

(1.3.3) Docker Compose establishes by default a communication network between β€œDocker Container 1” and β€œDocker Container 2” which allow the Node.js server process β€œtest-webapp” to communicate with the Redis database instance, and exchange between them the β€œnumber of visits to the app/web server” (numVisits) value.

(1.3.4) Docker Compose maps local hosting machine Port 80 to β€œDocker Container 1” Port 5000. Port 5000 is the port on which the Node.js server β€œtest-webapp” listens and reacts to the GETcommands sent by the browser.

(1.4) Connecting to the shell of β€œDocker Container 2” and then to the client command line of the Redis database instance via β€œredis-cli” we can see that the value of numVisits (which represents the number of times the browser issued a GETcommand to the Node.js server) is in sync with the value displayed in the browser by the Node.js server – thus showing that inter-process communication occurs between the processes β€œtest-webapp” in β€œDocker Container 1” and the Redis process in β€œDocker Container 2”.

(1.5) This step illustrates the restart directive and capability in Docker Compose (specified in config file β€œdocker-compose.yml”) – when connecting to the Linux shell of β€œDocker Container 1”, we can kill -9 the Node.js server process, but the Node.js server process will be restarted automatically by Docker Compose – illustrating the automatic recovery provided by Docker Compose.

And now let’s describe the steps and the flow of this scenario. The numbering scheme in the description below (i.e. (1.1), (1.2), (1.3) etc.), matches the numbering in β€œFigure 1.a – Schematic diagram of the components”.

(1.1) File structure:

Figure 1.b – File structure for Stage 1
Figure 1.b – File structure for Stage 1

Node.js files for process β€˜test-webapp’:

The contents of directory β€œtest-webapp”, where the source code for the Node.js server β€œtest-webapp” resides:

Node.js files for process 'test-webapp'

(1.2) The Dockerfile _containerizes and controls the Node.js application by downloading the β€œ_node:alpine” image from Docker Hub, installing Node.js on the container, copying to the container the source files – then launching the Node.js server web app (see source code in file β€œserver.js”).

(1.3) Going one directory above, we see the "docker-compose.yml" file that organizes the containerization and sets up the architecture of all the components. (File
β€œdocker-composer-nginx.yml” will be presented and explained in Stage 2 of this article)

'docker-compose.yml' file for process 'test-webapp'

Purge all images and containers:

We run command docker system prune -a to clear all Docker images and containers and start with a clean slate.

C:\test-docker\test-redis>docker system prune -a                                          
WARNING! This will remove:                                                                                                
- all stopped containers                                                                                                
- all networks not used by at least one container                                                                       
- all images without at least one container associated to them                                                          
- all build cache                                                                                                                                                                                                                             
Are you sure you want to continue? [y/N] y
Enter fullscreen mode Exit fullscreen mode

(1.3) Build and run the 'test-webapp' image with Docker Compose

Use command docker-compose -f <config-filename> build to build containers and the applications that will be running in each container:

C:\test-docker\test-redis>docker-compose -f docker-compose.yml build

See the results below of the built Docker image:

C:\test-docker\test-redis>docker images                                                                                                                     
REPOSITORY               TAG       IMAGE ID       CREATED         SIZE                                                                                                                     
test-redis_test-webapp   latest    e8145bea0fec   4 minutes ago   175MB
Enter fullscreen mode Exit fullscreen mode

Run the 'test-webapp' and 'redis' containers with 'docker-compose':

Let’s launch both β€œtest-webapp” and β€œredis” services, as described in config file
β€œdocker-compose.yml”, using the docker-compose -f <config-filename> up command.

docker-compose -f docker-compose.yml up

We can see from the output above, that both the β€œredis” container (β€œtest-redis_1” – corresponding to β€œDocker Container 2” in Figure 1.a) and the β€œtest-webapp” container (β€œtest-webapp_1” corresponding to β€œDocker Container 1” in Figure 1.a) are running and printing to stdout in the command line window where we launched Docker Compose to run these two containers.

View the 'test-webapp' and 'redis' running containers:

C:\test-docker\test-redis\test-webapp>docker ps                                                                                        
CONTAINER ID   IMAGE                    PORTS                
NAMES                                         
928b8b07415d   test-redis_test-webapp   0.0.0.0:80->5000/tcp   test-redis_test-webapp_1                      
a8756127bff5   redis:alpine             6379/tcp               test-redis_test-redis_1
Enter fullscreen mode Exit fullscreen mode

(1.3.1, 1.3.2) The two containers above match the containers β€œDocker Container 1” and β€œDocker Container 2” in the Figure 1.a above. Note the β€œCONTAINER ID” column whose values we will use below to perform operation on each individual running container.

(1.3.4) Port 5000 in the Node.js server "test-webapp" container is mapped to local (hosting) Port 80, so when one connects in the local (hosting) browser to URL http://localhost:80, for each refresh, the Node.js process in the β€œtest-webapp” container increments the number of visits in variable numVisits which is set and saved in the Redis in variable numVisits -- and this value is also send back and displayed in the browser.

β€œDocker-compose” sets-up by default a network with both β€œtest-webapp” container (β€œDocker Container 1” in Figure 1.a) and β€œredis” container (β€œDocker Container 2” in Figure 1.a) within this network, and both containers are reachable by each other via this network.

The local browser communicates with the Node.js server container. When refreshing the connection in the browser, the server callback is invoked which responds to the browser with the updated number of visits.

1.3.4 Browser - localhost - number of visits 8

(1.4) We are using the docker exec -it command that allows us to connect to a running container while the -it option allows us to capture the stdin/stdout of that container. Then we specify the CONTAINER ID a8756127bff5 obtained from docker ps command above, followed by the shell (sh) that we want to launch as we enter the container.

C:\test-redis\test-webapp>docker exec -it a8756127bff5 sh

Then, once we are inside the container’s shell, we connect to the Redis database using the redis-cli command. At the Redis prompt we use get numVisits to obtain the value of the variable β€œnumVisits” inside β€œredis”. We can see that the β€œredis” instance communicates with the β€œtest-webapp” process in its respective container and the variable β€œnumVisits” in the Redis database instance is in sync with its value in the browser. In this case both have the value β€œ8”, because we refreshed 8 times the β€œlocalhost:80” URL thus issuing a GET command in the browser that is intercepted by the *Node.js server * which increments the β€œnumber of visits” (numVisits) variable. The β€œnumber of visits” value is sent back to the browser by the β€œtest-webapp” process which also saves the value in the β€œredis” database in variable numVisits).

/data # redis-cli                                                                                                                                                     
127.0.0.1:6379> get numVisits                                                                                                                                      
"8"                                                                                                                                                                    
127.0.0.1:6379> 
Enter fullscreen mode Exit fullscreen mode

From within the β€œredis-cli” in the β€œredis” container (β€œDocker Container 2”) we can also set in Redis manually the β€œnumVisits” variable to a random value of let’s say β€œ342”…

1.4 - redis - set num visits 342

…the numVisits variable is updated in the β€œtest-webapp” Node.js server (running in β€œDocker Container 1”), and therefore in the browser (due to the fact that in order to invoke the callback in the Node.js server, one needs to refresh the connection to β€œlocalhost:80”, the number of visits increases by 1, thus 342 + 1 = 343. This shows that we have two-way inter-process communications between the processes running in β€œDocker Container 1” and β€œDocker Container 2”.

1.4 Browser - localhost - number of visits 343

(1.5) A useful feature provided by Docker Compose is the capability to specify in β€œdocker-compose.yml” a β€œrestart” option.
This will allow us when connecting to the shell of β€œDocker Container 1”, to β€œkill” the Node.js server process, but the Node.js server process will be restarted automatically by the Docker Compose β€œrestart” directive.

C:\test-docker\test-redis>docker ps
CONTAINER ID   IMAGE                      PORTS                    NAMES
c675ff6c0464   test-redis_nginx           0.0.0.0:80->80/tcp       test-redis_nginx_1
3137d1468ec7   test-redis_test-webapp-2   0.0.0.0:3009->5000/tcp   test-redis_test-webapp-2_1 
57d399295421   redis:alpine                                        test-redis_test-redis_1
b30635f44151   test-redis_test-webapp-1   0.0.0.0:3008->5000/tcp   test-redis_test-webapp-1_1
Enter fullscreen mode Exit fullscreen mode

Connect to the Docker container whose ID is 928b8b07415d and invoke the shell (sh).

C:\test-redis\test-webapp>docker exec -it 928b8b07415d sh

Inside the container, at the shell prompt, show all process id’s using ps -al.

/usr/src/app # ps -al
PID   USER     TIME  COMMAND
1     root     0:00  npm start
19    root     0:00  node server.js
30    root     0:00  sh
36    root     0:00  ps -al
Enter fullscreen mode Exit fullscreen mode

Proceed with β€œkilling” the β€œnode server.js” process by issuing a kill -9 <process-id> command:

/usr/src/app # kill -9 19

In the command line window that is running Docker Compose we can see how the β€œtest-webapp” receives a β€œkill signal” (SIGKILL), exited with code β€˜1’, and then restarted automatically.

npm ERR command sh -c node server.js

Conclusion

In Stage 1 of this example we showed how Docker Compose allows us to easily establish independent environments that communicate with each other, and also the automatic fault-tolerance (restart on failure) capability of Docker Compose.

Let’s continue with Stage 2:
(2) Load-balancing the Node.js server, with the help of a containerized Nginx reverse-proxy

The diagram in β€œFigure 2.a – Schematic diagram of the components for Stage 2” describes an architecture similar to the one described earlier in β€œFigure 1.a – Schematic diagram of the components” but with the changes described below.

Figure 2.a – Schematic diagram of the components for Stage 2
Figure 2.a – Schematic diagram of the components for Stage 2

In β€œFigure 2.a – Schematic diagram of the components for Stage 2” we have the following components:

(2.1.1, 2.1.2) β€œDocker Container 1” and β€œDocker Container 2” – two identical containers whose source code reside in directories β€œtest-webapp-1” and β€œtest-webapp-2” (as shown in β€œFigure 2.b – File structure for Stage 2” below), that are almost identical copies of the application β€œtest-webapp” that was described earlier in Stage 1. This time we are using two Node.js server processes that will serve the client browser from the local host machine, scaling up and load-balancing the original one-server configuration from Stage 1. These two containers are defined and controlled each by their respective β€œDockerfile” (2.1.1.1) and (2.1.1.2). Each Node.js server β€œDocker Container 1” and β€œDocker Container 2” counts the number of visits coming from the local host browser. Then it saves the number of visits into the Redis database, and it also responds back to the browser with the number of visits and with which specific Node.js server served each individual HTTP GET request coming from the browser, by sending back to the browser a message of type:
β€œtest-webapp-1: Number of visits is: ”, or
β€œtest-webapp-2: Number of visits is: ”
…thus highlighting the load-leveling nature of this stage.

(2.1.3) β€œDocker Container 3” – the container running the Redis database instance, identical to the one described in Stage 1, storing the β€œnumber of visits” performed by the localhost machine browser to β€œlocalhost:80”. The number of visits is stored by the Node.js server processes β€œtest-webapp-1” and β€œtest-webapp-2” in the Redis variable numVisits whose value is transmitted by each Node.js server to the Redis database on each refresh on the local host browser.

(2.2) β€œdocker-compose-nginx.yml” – the main Docker Compose config file defines and controls: (I) β€œDocker Container 1” running Node.js server β€œtest-webapp-1”, (II) β€œDocker Container 2” running Node.js server β€œtest-webapp-2”, (III) β€œDocker Container 3” running Redis, and (IV) β€œDocker Container 4” running Nginx.

(2.3) β€œDocker Container 4” running β€œNginx” – This is an additional container introduced in Stage 2, defined and controlled by its own Dockerfile (2.3.1), that runs an β€œnginx” instance, and acts a as reverse-proxy that routes the HTTP GET requests coming from the local host browser. The β€œNginx” process in β€œDocker Container 4” routes the HTTP GET requests coming from local host browser β€œlocalhost:80”, in a round-robin manner ((2.3.3) and (2.3.4)), to either the β€œtest-webapp-1” Node.js server in β€œDocker Container 1” or to β€œtest-webapp-2” Node.js server in β€œDocker Container 2”. The β€œnginx” process in β€œDocker Container 4” is defined and controlled by the _Nginx _config file β€œnginx.conf” which is copied by Nginx container’s Dockerfile to the β€œDocker Container 4” environment file β€œ/etc/nginx/conf.d./default.conf” (this is a standard Nginx setup). The β€œnginx” instance distributes the incoming traffic from the local host browser, thus scaling up and load- balancing the single-container web/app server architecture presented in Stage 1.

And now let’s describe the steps and the flow of this scenario. The numbering scheme in the description below (i.e. (2.1), (2.2), (2.3) etc.), matches the numbering in β€œFigure 2.a – Schematic diagram of the components for Stage 2”.

(2.1) File structure:

Figure 2.b – File structure for Stage 2

The file structure described in β€œFigure 2.b – File structure for Stage 2” is almost identical to the files structure described earlier in β€œFigure 1.b – File structure for Stage 1” with the following changes:

(2.1.1, 2.1.2) The files from directory β€œtest-webapp” from Stage 1 were copied into directories β€œtest-webapp-1” and β€œtest-webapp-2”.

(2.2) Going one directory above, we see the "docker-compose-nginx.yml" config file that organizes the containerization and sets up the architecture of all the components:

'docker-compose-nginx.yml' file

Purge all images and containers:

As in Stage 1, we run command docker system prune -a to clear all Docker images and containers and start with a clean slate.

(2.3) Build and run the 'test-webapp-1', 'test-webapp-2', β€˜redis’, and β€˜nginx’ images with Docker Compose

Build with Docker Compose:

C:\test-docker\test-redis>docker-compose -f docker-compose-nginx.yml build

Run with Docker Compose:

C:\test-docker\test-redis>docker-compose -f docker-compose-nginx.yml up

In the command line window where we issue the docker-compose -f docker-compose-nginx.yml up command, Docker Compose replies with:

4 containers running

...showing that all 4 Docker containers have started successfully and are up and running: β€œtest-redis_1” corresponds to the Redis process running in β€œDocker Container 3”, β€œtest-webapp-2_1” corresponds to the Node.js server process running in β€œDocker Container 2”, β€œtest-webapp-1_1” corresponds to the Node.js server process running in β€œDocker Container 1”, and β€œnginx_1” corresponds to the Nginx server running in β€œDocker Container 4”.

View the 'test-webapp-1', β€˜test-webapp-2’, 'redis', and β€˜nginx’ running containers:

C:\test-docker\test-redis>docker ps 
CONTAINER ID   IMAGE                       PORTS       NAMES                                            c675ff6c0464   test-redis_nginx            0.0.0.0:80->80/tcp        test-redis_nginx_1                               
3137d1468ec7   test-redis_test-webapp-2    0.0.0.0:3009->5000/tcp   
test-redis_test-webapp-2_1                       
57d399295421   redis:alpine                                                                         test-redis_test-redis_1                          
b30635f44151   test-redis_test-webapp-1    0.0.0.0:3008->5000/tcp   test-redis_test-webapp-1_1
Enter fullscreen mode Exit fullscreen mode

The four containers above match containers β€œDocker Container 1” through β€œDocker Container 4” in β€œFigure 2.a – Schematic diagram of the components for Stage 2”
above. Note the β€œCONTAINER ID” column whose values we will use below to potentially perform operations on each individual running container.

Let’s run first two instances of the browser on the hosting machine, and point them to URL β€œlocalhost:80”:

2.3 Browser - test-webapp-2 - number of visits 7

2.3 Browser - test-webapp-1 - number of visits 8

Notice how due to the round-robin routing mechanism employed by the Nginx reverse-proxy, the β€œGET localhost:80” request is routed once to β€œtest-webapp-1” Node.js server, and once to the β€œtest-webapp-2” Node.js server, achieving the scaling-up and load balancing that we intended to demonstrate.

Let’s connect to the container that is running Redis, to its sh (shell) environment:

C:\test-docker\test-redis>docker exec -it 57d399295421 sh

Then, inside the container, let’s connect to Redis itself using β€œredis-cli”:

/data #
/data # redis-cli
127.0.0.1:6379> 
127.0.0.1:6379> get numVisits
"8"
127.0.0.1:6379>
Enter fullscreen mode Exit fullscreen mode

Note how the get numVisits command in Redis returns the expected value of β€œnumber of visits” that is communicated to the β€œredis” container from the containers that are running the Node.js app servers.

Conclusion

In Stage 2 of this example we showed how Docker Compose allows us to easily establish multiple containers with their independent environments that communicate with each other, and also how scaling and load-balancing achieved with Nginx.

Source code:
https://github.com/marcelkatz/test-docker-nodejs-redis-nginx

Top comments (3)

Collapse
 
franelfers profile image
Francael

Nice work
I couldn't access Redis from any container, and for some reason and after a day of searching and testing, seeing your code working was a glare of hope but I figured out it's using node-redis 3.1.2, I'm still trying to do this exact same project using Redis 4.1 with no luck.

Collapse
 
franelfers profile image
Francael

I figured it out, I had to connect to the container NAME, instead of localhost.
Redis.createClient({url:'redis://:6379'})
Your post helped me a lot, thanks

Collapse
 
stephanleiser profile image
Stephan Leiser

Very helpful tutorial to get started with the topic. Highly appreciated! Thank you.

Create an Account! πŸ‘€ Just want to lurk?
Β 
You can still create an account and turn on features like 🌚 dark mode.