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
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 GET
command 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 GET
commands 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 GET
command 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
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:
(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)
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
(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
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.
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
(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.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>
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ββ¦
β¦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.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
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
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.
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
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:
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:
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:
...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
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β:
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>
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)
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.
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
Very helpful tutorial to get started with the topic. Highly appreciated! Thank you.