Professional containerization of Node.js applications using Docker

Original author: Ankit Jain
  • 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.

image



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.jsonthis 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.jsin 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 /appduring 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_ENVset to production) npm will not install the modules listed in the devDependenciesfile section package.json.

# Установка зависимостей
COPY package*.json ./
RUN npm install
# Для использования в продакшне
# RUN npm install --production

Here we are copying to a file image package*.jsoninstead of, for example, copying all the project files. We are doing it because of the fact that Dockerfile instructions RUN, COPYand ADDcreate 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 ARGthat 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-applistens 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 .dockerignorecommand, 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, ADDand COPYcreate 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 RUNthat 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-2it 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 installsystem 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 updateand installto 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.


Also popular now: