Professional containerization of Node.js applications using Docker
- Transfer
- Tutorial
The author of the material, the translation of which we publish today, is a DevOps engineer. He says he has to use Docker . In particular, this container management platform is used at various stages of the life cycle of Node.js applications. Using Docker, a technology that, recently, has been extremely popular, allows you to optimize the development and output process of Node.js projects in production.
Now we are publishing a series of articles about Docker, designed for those who want to learn this platform for its use in a variety of situations. The same material focuses mainly on the professional use of Docker in Node.js development.
Docker is a program that is designed to organize virtualization at the operating system level (containerization). At the heart of containers are layered images. Simply put, Docker is a tool that allows you to create, deploy, and run applications using containers independent of the operating system on which they run. The container includes an image of the base OS necessary for the application to work, the library on which this application depends, and this application itself. If several containers are running on the same computer, then they use the resources of this computer together. Docker containers can pack projects created using a variety of technologies. We are interested in projects based on Node.js.
Before we pack a Node.js project into a Docker container, we need to create this project. Let's do it. Here is the file for
To install the project dependencies, execute the command
As you can see, here we described a simple server that returns some text in response to requests to it.
Now that the application is ready, let's talk about how to pack it into a Docker container. Namely, it will be about what is the most important part of any Docker-based project, about the Dockerfile.
A Dockerfile is a text file that contains instructions for creating a Docker image for an application. The instructions in this file, if not going into details, describe the creation of layers of a multilevel file system, which has everything that an application needs to work. The Docker platform can cache image layers, which, when reusing layers that are already in the cache, speeds up the process of building images.
In object-oriented programming, there is such a thing as a class. Classes are used to create objects. In Docker, images can be compared with classes, and containers can be compared with instances of images, that is, with objects. Consider the process of generating a Dockerfile, which will help us figure this out.
Create an empty Dockerfile:
Since we are going to build a container for the Node.js application, the first thing we need to put in the container will be the basic Node image, which can be found on the Docker Hub . We will use the LTS version of Node.js. As a result, the first statement of our Dockerfile will be the following statement:
After that, create a directory for our code. At the same time, thanks to the instructions used here
Since we use the Node image, the Node.js and npm platforms will already be installed in it. Using what is already in the image, you can organize the installation of project dependencies. Using the flag
Here we are copying to a file image
Now copy the project files to the current working directory. Here we will not use the ADD instruction , but the COPY instruction . In fact, in most cases it is recommended to give preference to instructions
The instruction
Docker containers are isolated environments. This means that when we launch the application in the container, we will not be able to interact with it directly without opening the port that this application listens on. In order to inform Docker that there is an application in a certain container listening on a certain port, you can use the EXPOSE instruction .
To date, we, using the Dockerfile, have described the image that the application will contain and everything that it needs to successfully launch. Now add the instruction to the file that allows you to start the application. This is a CMD instruction . It allows you to specify a certain command with parameters that will be executed when the container starts, and, if necessary, can be overridden by command line tools.
Here's what the finished Dockerfile will look like:
We have prepared a Dockerfile file that contains instructions for building the image, on the basis of which a container with a running application will be created. Assemble the image by executing a command of the following form:
In our case, it will look like this:
Dockerfile has an instruction
After assembling the image, check if Docker sees it. To do this, run the following command:
In response to this command, approximately the following should be output.
Docker Images
After we have assembled the Docker image, we can run it, that is, create an instance of it, represented by a working container. To do this, use a command of this kind:
In our case, it will look like this:
We will ask the system for information about working containers using this command:
In response to this, the system should output something like the following:
Docker containers
So far, everything is going as expected, although we have not yet tried to access the application running in the container. Namely, our container having a name
If the container really works, then something like the one shown in the following figure will be returned in response to this command.
The result of checking the health of the container
On the basis of the same image, for example, on the basis of just created, you can create many containers. In addition, you can send our image to the Docker Hub registry, which will enable other developers to upload our image and launch the appropriate containers at home. This approach simplifies working with projects.
Here are some suggestions worth considering in order to leverage the power of Docker and create as compact images as possible.
In the project folder that you plan to place in the container, you always need to create a file
Consider the example when we collect a project for a certain organization. This project uses many npm packages, and each such package can install additional packages on which it depends. Performing all these operations leads to additional time spent in the process of assembling the image (although this, thanks to Docker's caching capabilities, is not such a big deal). Worse, the resulting image containing the dependencies of a certain project is quite large. Here, if we are talking about front-end projects, we can recall that such projects are usually processed using bundlers like webpack, which make it possible to conveniently pack everything that an application needs in a sales package. As a result, npm package files for such a project are unnecessary. And this means that from such files we,
Armed with this idea, try to do this:
Such an approach, however, will not suit us. As we have said, the instructions
In this example, there is only one instruction
Imagine that we are working on a frontend project that has many dependencies, and we use webpack to build this project. With this approach, we can, for the sake of reducing the size of the image, take advantage of the capabilities of Docker for multi-stage assembly of images .
With this approach, the final image is much smaller than the previous image, and, in addition, we use the image
Comparing images from the Node repository
Strive to use Docker’s caching capabilities to build your images. We already paid attention to this opportunity, working with a file that was accessed by name
Suppose we describe in Dockerfile installing packages in an image created from a base image
When the system will process this file, if there are a lot of installed packages, the update and installation operations will take a lot of time. In order to improve the situation, we decided to take advantage of the layer caching capabilities of Docker and rewrote the Dockerfile as follows:
Now, when assembling the image for the first time, everything goes as it should, since the cache has not yet been formed. Imagine now that we need to install another package
As a result of the execution of such a command,
It is recommended, whenever possible, to strive to minimize the number of image layers, since each layer is the file system of the Docker image, which means that the smaller the layers in the image, the more compact it will be. When using the multi-stage process of image assembly, a reduction in the number of layers in the image and a reduction in the size of the image are achieved.
In this article, we looked at the process of packaging Node.js applications in Docker containers and working with such containers. In addition, we made some recommendations that, by the way, can be used not only when creating containers for Node.js projects.
Dear readers! If you professionally use Docker when working with Node.js projects, please share recommendations on the effective use of this system with beginners.
What is a docker?
Docker is a program that is designed to organize virtualization at the operating system level (containerization). At the heart of containers are layered images. Simply put, Docker is a tool that allows you to create, deploy, and run applications using containers independent of the operating system on which they run. The container includes an image of the base OS necessary for the application to work, the library on which this application depends, and this application itself. If several containers are running on the same computer, then they use the resources of this computer together. Docker containers can pack projects created using a variety of technologies. We are interested in projects based on Node.js.
Creating a Node.js project
Before we pack a Node.js project into a Docker container, we need to create this project. Let's do it. Here is the file for
package.json
this project:{
"name": "node-app",
"version": "1.0.0",
"description": "The best way to manage your Node app using Docker",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"author": "Ankit Jain ",
"license": "ISC",
"dependencies": {
"express": "^4.16.4"
}
}
To install the project dependencies, execute the command
npm install
. During the operation of this command, among other things, a file will be created package-lock.json
. Now create a file index.js
in which the project code will be located:const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('The best way to manage your Node app using Docker\n');
});
app.listen(3000);
console.log('Running on http://localhost:3000');
As you can see, here we described a simple server that returns some text in response to requests to it.
Create Dockerfile
Now that the application is ready, let's talk about how to pack it into a Docker container. Namely, it will be about what is the most important part of any Docker-based project, about the Dockerfile.
A Dockerfile is a text file that contains instructions for creating a Docker image for an application. The instructions in this file, if not going into details, describe the creation of layers of a multilevel file system, which has everything that an application needs to work. The Docker platform can cache image layers, which, when reusing layers that are already in the cache, speeds up the process of building images.
In object-oriented programming, there is such a thing as a class. Classes are used to create objects. In Docker, images can be compared with classes, and containers can be compared with instances of images, that is, with objects. Consider the process of generating a Dockerfile, which will help us figure this out.
Create an empty Dockerfile:
touch Dockerfile
Since we are going to build a container for the Node.js application, the first thing we need to put in the container will be the basic Node image, which can be found on the Docker Hub . We will use the LTS version of Node.js. As a result, the first statement of our Dockerfile will be the following statement:
FROM node:8
After that, create a directory for our code. At the same time, thanks to the instructions used here
ARG
, we can, if necessary, specify the name of the application directory other than /app
during assembly of the container. Details about this manual can be found here .# Папка приложения
ARG APP_DIR=app
RUN mkdir -p ${APP_DIR}
WORKDIR ${APP_DIR}
Since we use the Node image, the Node.js and npm platforms will already be installed in it. Using what is already in the image, you can organize the installation of project dependencies. Using the flag
--production
(or if the environment variable is NODE_ENV
set to production
) npm will not install the modules listed in the devDependencies
file section package.json
.# Установка зависимостей
COPY package*.json ./
RUN npm install
# Для использования в продакшне
# RUN npm install --production
Here we are copying to a file image
package*.json
instead of, for example, copying all the project files. We are doing it because of the fact that Dockerfile instructions RUN
, COPY
and ADD
create additional layers of the image, so you can harness the power of caching layers Docker platform. With this approach, the next time we collect a similar image, Docker will find out whether it is possible to reuse image layers that are already in the cache, and if so, it will take advantage of what is already there, instead of creating new ones layers. This allows you to seriously save time when assembling layers in the course of work on large projects, which include many npm modules.Now copy the project files to the current working directory. Here we will not use the ADD instruction , but the COPY instruction . In fact, in most cases it is recommended to give preference to instructions
COPY
. The instruction
ADD
, in comparison with COPY
, has some features, which, however, are not always needed. For example, we are talking about options for unpacking .tar archives and downloading files by URL.# Копирование файлов проекта
COPY . .
Docker containers are isolated environments. This means that when we launch the application in the container, we will not be able to interact with it directly without opening the port that this application listens on. In order to inform Docker that there is an application in a certain container listening on a certain port, you can use the EXPOSE instruction .
# Уведомление о порте, который будет прослушивать работающее приложение
EXPOSE 3000
To date, we, using the Dockerfile, have described the image that the application will contain and everything that it needs to successfully launch. Now add the instruction to the file that allows you to start the application. This is a CMD instruction . It allows you to specify a certain command with parameters that will be executed when the container starts, and, if necessary, can be overridden by command line tools.
# Запуск проекта
CMD ["npm", "start"]
Here's what the finished Dockerfile will look like:
FROM node:8
# Папка приложения
ARG APP_DIR=app
RUN mkdir -p ${APP_DIR}
WORKDIR ${APP_DIR}
# Установка зависимостей
COPY package*.json ./
RUN npm install
# Для использования в продакшне
# RUN npm install --production
# Копирование файлов проекта
COPY . .
# Уведомление о порте, который будет прослушивать работающее приложение
EXPOSE 3000
# Запуск проекта
CMD ["npm", "start"]
Image assembly
We have prepared a Dockerfile file that contains instructions for building the image, on the basis of which a container with a running application will be created. Assemble the image by executing a command of the following form:
docker build --build-arg -t /: /path/to/Dockerfile
In our case, it will look like this:
docker build --build-arg APP_DIR=var/app -t ankitjain28may/node-app:V1 .
Dockerfile has an instruction
ARG
that describes an argument APP_DIR
. Here we set its meaning. If this is not done, then it will take the value that is assigned to it in the file, that is - app
. After assembling the image, check if Docker sees it. To do this, run the following command:
docker images
In response to this command, approximately the following should be output.
Docker Images
Image Launch
After we have assembled the Docker image, we can run it, that is, create an instance of it, represented by a working container. To do this, use a command of this kind:
docker run -p -d --name /:
In our case, it will look like this:
docker run -p 8000:3000 -d --name node-app ankitjain28may/node-app:V1
We will ask the system for information about working containers using this command:
docker ps
In response to this, the system should output something like the following:
Docker containers
So far, everything is going as expected, although we have not yet tried to access the application running in the container. Namely, our container having a name
node-app
listens on a port 8000
. In order to try to access it, you can open a browser and go to it at the address localhost:8000
. In addition, in order to check the health of the container, you can use the following command:curl -i localhost:8000
If the container really works, then something like the one shown in the following figure will be returned in response to this command.
The result of checking the health of the container
On the basis of the same image, for example, on the basis of just created, you can create many containers. In addition, you can send our image to the Docker Hub registry, which will enable other developers to upload our image and launch the appropriate containers at home. This approach simplifies working with projects.
Recommendations
Here are some suggestions worth considering in order to leverage the power of Docker and create as compact images as possible.
▍1. Always create a .dockerignore file
In the project folder that you plan to place in the container, you always need to create a file
.dockerignore
. It allows you to ignore files and folders that are not needed when building the image. With this approach, we can reduce the so-called build context, which will allow us to quickly assemble the image and reduce its size. This file supports file name templates, in this it looks like a file .gitignore
. It is recommended to add to the .dockerignore
command, thanks to which Docker will ignore the folder /.git
, since this folder usually contains large materials (especially during the development of the project) and adding it to the image leads to an increase in its size. In addition, copying this folder into an image does not make much sense.▍2. Use the multi-stage image assembly process
Consider the example when we collect a project for a certain organization. This project uses many npm packages, and each such package can install additional packages on which it depends. Performing all these operations leads to additional time spent in the process of assembling the image (although this, thanks to Docker's caching capabilities, is not such a big deal). Worse, the resulting image containing the dependencies of a certain project is quite large. Here, if we are talking about front-end projects, we can recall that such projects are usually processed using bundlers like webpack, which make it possible to conveniently pack everything that an application needs in a sales package. As a result, npm package files for such a project are unnecessary. And this means that from such files we,
Armed with this idea, try to do this:
# Установка зависимостей
COPY package*.json ./
RUN npm install --production
# Продакшн-сборка
COPY . .
RUN npm run build:production
# Удаление папки с npm-модулями
RUN rm -rf node_modules
Such an approach, however, will not suit us. As we have said, the instructions
RUN
, ADD
and COPY
create layers cached Docker, so we need to find a way to deal with the installation of dependencies, build the project and then removing the unnecessary files with a single command. For example, it might look like this:# Добавляем в образ весь проект
COPY . .
# Устанавливаем зависимости, собираем проект и удаляем зависимости
RUN npm install --production && npm run build:production && rm -rf node_module
In this example, there is only one instruction
RUN
that installs the dependencies, builds the project, and deletes the folder node_modules
. This leads to the fact that the size of the image will not be as large as the size of the image that includes the folder node_modules
. We use the files from this folder only during the build process of the project, and then delete it. True, this approach is bad in that it takes a lot of time to install npm dependencies. You can eliminate this drawback using the technology of multi-stage assembly of images. Imagine that we are working on a frontend project that has many dependencies, and we use webpack to build this project. With this approach, we can, for the sake of reducing the size of the image, take advantage of the capabilities of Docker for multi-stage assembly of images .
FROM node:8 As build
# Папки
RUN mkdir /app && mkdir /src
WORKDIR /src
# Установка зависимостей
COPY package*.json ./
RUN npm install
# Для использования в продакшне
# RUN npm install --production
# Копирование файлов проекта и сборка проекта
COPY . .
RUN npm run build:production
# В результате получается образ, состоящий из одного слоя
FROM node:alpine
# Копируем собранные файлы из папки build в папку app
COPY --from=build ./src/build/* /app/
ENTRYPOINT ["/app"]
CMD ["--help"]
With this approach, the final image is much smaller than the previous image, and, in addition, we use the image
node:alpine
, which itself is very small. But a comparison of a pair of images, during which it is clear that the image is node:alpine
much smaller than the image node:8
.Comparing images from the Node repository
▍3. Use Docker Cache
Strive to use Docker’s caching capabilities to build your images. We already paid attention to this opportunity, working with a file that was accessed by name
package*.json
. This reduces the build time of the image. But this opportunity should not be used rashly. Suppose we describe in Dockerfile installing packages in an image created from a base image
Ubuntu:16.04
:FROM ubuntu:16.04
RUN apt-get update && apt-get install -y \
curl \
package-1 \
.
.
When the system will process this file, if there are a lot of installed packages, the update and installation operations will take a lot of time. In order to improve the situation, we decided to take advantage of the layer caching capabilities of Docker and rewrote the Dockerfile as follows:
FROM ubuntu:16.04
RUN apt-get update
RUN apt-get install -y \
curl \
package-1 \
.
.
Now, when assembling the image for the first time, everything goes as it should, since the cache has not yet been formed. Imagine now that we need to install another package
package-2
. To do this, we rewrite the file:FROM ubuntu:16.04
RUN apt-get update
RUN apt-get install -y \
curl \
package-1 \
package-2 \
.
.
As a result of the execution of such a command,
package-2
it will not be installed or updated. Why? The fact is that when executing the instruction RUN apt-get update
, Docker does not see any difference between this instruction and the instruction executed earlier, as a result, it takes data from the cache. And this data is already outdated. When processing an instruction, the RUN apt-get install
system executes it; for it, it does not look like a similar instruction in the previous Dockerfile, but during the installation, errors may occur or the old version of packages will be installed. As a result, it turns out that the team update
and install
to be performed within a single statement RUN
, as done in the first example. Caching is a great feature, but reckless use of this feature can lead to problems.▍4. Minimize the number of image layers
It is recommended, whenever possible, to strive to minimize the number of image layers, since each layer is the file system of the Docker image, which means that the smaller the layers in the image, the more compact it will be. When using the multi-stage process of image assembly, a reduction in the number of layers in the image and a reduction in the size of the image are achieved.
Summary
In this article, we looked at the process of packaging Node.js applications in Docker containers and working with such containers. In addition, we made some recommendations that, by the way, can be used not only when creating containers for Node.js projects.
Dear readers! If you professionally use Docker when working with Node.js projects, please share recommendations on the effective use of this system with beginners.