Docker Tips : Development With Nodemon

TL;DR

I have delivered several Docker trainings during the last few weeks and some questions come back quite often. One of them is this one:

Q: When developing my application, how can I get my changes to be taken into account automatically in the containers ?

The idea behind this question is, for a developer, to make changes in his/her local IDE (Atom, Visual Studio Code, Eclipse, vi…) and then see the changes taken into account in realtime in the running application.

Usually, I come with the following answer.

R: While in development, you need to mount the local source code in the services’ containers and start the main process through a utility like Nodemon which will watch files and restart the application when some changes are observed.

Last time, I gave a similar answer and then realized this deserved some additional details and examples. This is what this post is all about.

Example application

Let’s consider the Docker Voting Application, this one is used very often for demos and presentations.

This application follows a micro-services architecture. It is made of 5 services as illustrated below.

Docker’s voting app architecture (https://github.com/docker/example-voting-app)
  • vote: front-end that enables a user to choose between a cat and a dog
  • redis: database where votes are stored
  • worker: service that get votes from redis and store the results in a postgres database
  • db: the postgres database in which vote’s results are stored
  • result: front-end displaying the results of the vote

Let’s clone the Voting App repository

git clone https://github.com/dockersamples/example-voting-app

As we can see, several Docker Compose files are defined here. We’ll use the default one, named docker-compose.yml in this article.

version: "3.3"

services:
vote:
build: ./vote
command: python app.py
volumes:
- ./vote:/app
ports:
- "5000:80"
networks:
- front-tier
- back-tier

result:
build: ./result
command: nodemon server.js
volumes:
- ./result:/app
ports:
- "5001:80"
- "5858:5858"
networks:
- front-tier
- back-tier

worker:
build:
context: ./worker
depends_on:
- "redis"
networks:
- back-tier

redis:
image: redis:alpine
container_name: redis
ports: ["6379"]
networks:
- back-tier

db:
image: postgres:9.4
container_name: db
volumes:
- "db-data:/var/lib/postgresql/data"
networks:
- back-tier

volumes:
db-data:

networks:
front-tier:
back-tier:

Illustration on the result service

Let’s have a closer look at the way the result service is defined in this file.

result:
build: ./result
command: nodemon server.js
volumes:
- ./result:/app
ports:
- "5001:80"
- "5858:5858"
networks:
- front-tier
- back-tier

Several interesting things here:

  • the local result folder is bind-mounted into the /app folder
  • the command used to run the service is nodemon server.js

Bind-mounting the source folder

The local source code, the one we continuously change within our favorite IDE will be available as-is within the service’s container. In other words, every change we will do locally will be reflected in the running application but if we want it to be taken into account by the application, this one needs to be reloaded. Enter nodemon !

Running the service with Nodemon

Nodemon is a great piece of software, the official description is the following one.

Nodemon is a utility that will monitor for any changes in your source and automatically restart your server. Perfect for development.

In other words, this guy is there to supervise the main process running in the container and to restart it if it detects some changes on some surrounding files.

The result service is developed with Node.js so it’s very easy to run it with nodemon instead of with the default node command.

As we can see, the command defined in in the docker-compose.yml file overrides the one defined in the Dockerfile.

// Command instruction in the Dockerfile
CMD [“node”, “server.js”]
// Command instruction in the docker-compose.yml file
command: nodemon server.js

For nodemon to react on changes made on html files located in the views folder, we will change the command specified in the docker-compose.yml file a little bit and make it look like the following.

// Slight modification of the command instruction
command: nodemon --watch views -e js,html server.js

Of course, in order for the result service to start with nodemon, we need to have it available within the container. The instruction to install it in the Dockerfile of the result service is the following one.

RUN npm install -g nodemon

Not a Node.js application ?

nodemon integrates very well with a Node.js application, but what if the application we use is developed with another language ?

Not a problem… nodemon can run several types of applications. It also provides out of the box several options to watch changes into specific folders and allows to provide file extension as well. Let’s illustrate this on the vote service, this one is a python flask application.

  • The first thing to do is to make sure nodemon is available in the service’s image. To do so, we start by changing the Dockerfile of the service adding the following instruction:
# Install nodemon
RUN apk update && apk add nodejs && npm i -g nodemon

Note1: since Alpine 3.8, npm is not installed with nodejs. The nodejs-npm package needs to be added in the above command next to nodejs. Thanks Omar Quiroz for pointing this out.

Note2: as nodemon will not be needed inside the production image, it could be good to add a condition based on a build arg to it could be installed in dev only.

  • The second thing is to override the command used to run the service so it uses nodemon. The definition of the vote service can be changed to
services:
vote:
build: ./vote
command: nodemon --watch template --exec "python" app.py
volumes:
— ./vote:/app
ports:
— "5000:80"
networks:
— front-tier
— back-tier

We provide some additional options to nodemon

  • --exec “python”: indicates the type of application we need to run
  • --watch template: on top of the app.py script run, we want to watch changes done in the template folder

It is also possible to provide the list of extension we need to watch through a -e flag. The nodemon documentation provides a list of the available options.

Let’s test

We can now build the vote service so the changes we have done in the Dockerfile are taken into account, and then start the application.

$ docker-compose build vote
$ docker-compose up

The vote interface is available on port 5000, the result one on port 5001.

  • Modify the source code of the vote service

Let’s modify the selection options and change Cats into Kitten in the app.py file

option_a = os.getenv(‘OPTION_A’, “Kitten”)
option_b = os.getenv(‘OPTION_B’, “Dogs”)

We can observe the automatic reload of vote in the Compose logs.

vote_1 | * Detected change in ‘/app/app.py’, reloading
vote_1 | * Restarting with stat
vote_1 | * Debugger is active!
vote_1 | * Debugger PIN: 161–189–800

Reloading the web interface shows the changes

  • Modify the source code of the result service

Let’s also modify the labels which appear in the web interface of the result service. Those ones are located in the views/index.html file.

<head>
<meta charset="utf-8">
<title>Kitten vs Dogs -- Result</title>
...
</head>
<body ng-controller="statsCtrl" >
...
<div id="content-container">
<div id="content-container-center">
<div id="choice">
<div class="choice cats">
<div class="label">Kitten</div>
<div class="stat">{{aPercent | number:1}}%</div>
</div>
<div class="divider"></div>
<div class="choice dogs">
<div class="label">Dogs</div>
<div class="stat">{{bPercent | number:1}}%</div>
</div>
</div>
</div>
...

We can observe the automatic reload of result in the Compose logs.

result_1 | [nodemon] restarting due to changes…
result_1 | [nodemon] starting `node server.js`

Reloading the web interface shows the changes.

Summary

In this post, we have seen the setup needed for a developer to be able to work on the source code and have it automatically be taken into account into the application running in container.

Once we are happy with the code changes, we can use docker-compose to build and push the images to the registry and thus trigger the CI pipeline.