使用Docker的Node.js应用程序的专业容器化

该材料的作者是DevOps工程师,我们今天发布其译文。 他说他必须使用Docker 。 特别是,此容器管理平台用于Node.js应用程序生命周期的各个阶段。 使用Docker是一项最近非常流行的技术,它使您可以优化生产环境中Node.js项目的开发和输出过程。

图片

现在,我们正在发布一系列有关Docker 的文章 ,这些文章专为那些希望学习该平台以在各种情况下使用的人而设计。 相同的材料主要侧重于在Node.js开发中对Docker的专业使用。

什么是码头工人?


Docker是一个旨在在操作系统级别组织虚拟化(容器化)的程序。 容器的核心是分层图像。 简而言之,Docker是一种工具,可让您使用独立于运行容器的操作系统的容器来创建,部署和运行应用程序。 该容器包括应用程序运行所需的基本OS映像,该应用程序所依赖的库以及该应用程序本身。 如果多个容器在同一台计算机上运行,​​则它们将一起使用此计算机的资源。 Docker容器可以打包使用多种技术创建的项目。 我们对基于Node.js的项目感兴趣。

创建一个Node.js项目


在将Node.js项目打包到Docker容器中之前,我们需要创建此项目。 来吧 这是该项目的package.json文件:

 { "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 <ankitjain28may77@gmail.com>", "license": "ISC", "dependencies": {   "express": "^4.16.4" } } 

要安装项目依赖项,请运行npm install命令。 在此命令过程中,除其他外,还将创建package-lock.json文件。 现在创建index.js文件,其中将包含项目代码:

 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'); 

如您所见,这里我们描述了一个简单的服务器,该服务器返回一些文本以响应对其的请求。

创建Dockerfile


现在应用程序已准备就绪,让我们讨论如何将其打包到Docker容器中。 即,它将是关于任何基于Docker的项目中最重要的部分,即Dockerfile。

Dockerfile是一个文本文件,其中包含有关为应用程序创建Docker映像的说明。 该文件中的说明(如果不做详细介绍)描述了多层文件系统各层的创建,其中包含应用程序需要工作的所有内容。 Docker平台可以缓存图像层,当重新使用缓存中已存在的层时,可以加快构建图像的过程。

在面向对象的编程中,有一个类。 类用于创建对象。 在Docker中,可以将图像与类进行比较,并且可以将容器与图像实例即对象进行比较。 考虑一下生成Dockerfile的过程,这将有助于我们解决这一问题。

创建一个空的Dockerfile:

 touch Dockerfile 

由于我们将为Node.js应用程序构建一个容器,因此我们需要放入容器中的第一件事是基本的Node映像,该映像可在Docker Hub上找到。 我们将使用LTS版本的Node.js。 结果,我们的Dockerfile的第一条语句将是以下语句:

 FROM node:8 

之后,为我们的代码创建一个目录。 同时,由于这里使用了ARG指令,因此,在容器的组装过程中,如有必要,我们可以指定/app以外的应用程序目录的名称。 有关本手册的详细信息,请参见此处

 #   ARG APP_DIR=app RUN mkdir -p ${APP_DIR} WORKDIR ${APP_DIR} 

由于我们使用Node映像,因此已经安装了Node.js和npm平台。 使用映像中已存在的内容,可以组织项目依赖项的安装。 使用--production标志(或者如果将NODE_ENV环境NODE_ENV设置为production ),npm将不会安装devDependencies文件的devDependencies部分中列出的模块。

 #   COPY package*.json ./ RUN npm install #     # RUN npm install --production 

在这里,我们将package*.json文件复制到映像中,而不是例如复制所有项目文件。 我们这样做是因为Dockerfile RUNCOPYADD指令会创建其他映像层,因此您可以使用Docker平台层的缓存功能。 使用这种方法,下次我们收集类似的映像时,Docker将发现是否可以重用缓存中已经存在的映像层,如果可以,则将利用已有的映像层,而不是创建新的映像层层。 这使您可以在大型项目(包括许多npm模块)中的工作过程中组装层时,节省大量时间。

现在,将项目文件复制到当前工作目录。 在这里,我们将不使用ADD指令,而是使用COPY指令。 实际上,在大多数情况下,建议优先使用COPY指令。

COPY相比, ADD指令具有某些功能,但是并不总是需要这些功能。 例如,我们正在讨论用于解压缩.tar归档文件和通过URL下载文件的选项。

 #    COPY . . 

Docker容器是隔离的环境。 这意味着当我们在容器中启动应用程序时,如果不打开该应用程序侦听的端口,我们将无法直接与它进行交互。 为了通知Docker某个容器中有某个应用程序正在侦听某个端口,可以使用EXPOSE指令。

 #   ,      EXPOSE 3000 

到目前为止,我们已经使用Dockerfile描述了应用程序将包含的映像以及成功启动所需的一切。 现在,将指令添加到允许您启动应用程序的文件中。 这是CMD指令。 它允许您指定带有在容器启动时将执行的参数的特定命令,并且在必要时可以被命令行工具覆盖。

 #   CMD ["npm", "start"] 

这是完成的Dockerfile的外观:

 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"] 

图像组装


我们准备了一个Dockerfile文件,其中包含用于构建映像的说明,在此基础上,将创建一个具有运行中应用程序的容器。 通过执行以下形式的命令来组装图像:

 docker build --build-arg <build arguments> -t <user-name>/<image-name>:<tag-name> /path/to/Dockerfile 

在我们的情况下,它将如下所示:

 docker build --build-arg APP_DIR=var/app -t ankitjain28may/node-app:V1 . 

Dockerfile有一个描述参数APP_DIRARG语句。 在这里,我们设置其含义。 如果不这样做,它将采用文件中分配给它的值app

组装完映像后,检查Docker是否可见。 为此,请运行以下命令:

 docker images 

响应该命令,大约应输出以下内容。


Docker映像

图片启动


组装完Docker映像后,我们可以运行它,即创建一个实例,以工作容器表示。 为此,请使用以下命令:

 docker run -p <External-port:exposed-port> -d --name <name of the container> <user-name>/<image-name>:<tag-name> 

在我们的情况下,它将如下所示:

 docker run -p 8000:3000 -d --name node-app ankitjain28may/node-app:V1 

我们将使用以下命令询问系统有关工作容器的信息:

 docker ps 

响应于此,系统应输出如下内容:


Docker容器

到目前为止,一切都按预期进行,尽管我们尚未尝试访问容器中运行的应用程序。 即,我们的名为node-app容器在端口8000上侦听。 为了尝试访问它,您可以打开一个浏览器,然后在localhost:8000 。 另外,为了检查容器的运行状况,可以使用以下命令:

 curl -i localhost:8000 

如果该容器确实起作用,则将响应该命令返回下图所示的内容。


容器健康检查结果

基于相同的映像,例如基于刚刚创建的映像,可以创建许多容器。 另外,您可以将我们的映像发送到Docker Hub注册表,这将使其他开发人员可以上载我们的映像并在家里启动适当的容器。 这种方法简化了项目的工作。

推荐建议


这里有一些值得考虑的建议,以便利用Docker的功能并创建尽可能紧凑的映像。

▍1。 始终创建.dockerignore文件


在您计划放置在容器中的项目文件夹中,始终需要创建一个.dockerignore文件。 它使您可以忽略构建映像时不需要的文件和文件夹。 使用这种方法,我们可以减少所谓的构建上下文,这将使我们能够快速组装图像并减小其大小。 该文件支持文件名模板,类似于.gitignore文件。 建议将命令添加到.dockerignore ,因此Docker将忽略/.git文件夹,因为该文件夹通常包含较大的材质(尤其是在项目开发期间),并将其添加到映像中会导致其尺寸增加。 此外,将此文件夹复制到图像中没有多大意义。

▍2。 使用多阶段图像组装过程


当我们为某个组织收集项目时,请考虑该示例。 该项目使用许多npm软件包,每个这样的软件包都可以安装它依赖的其他软件包。 执行所有这些操作会导致在构建映像的过程中花费更多的时间(尽管这要归功于Docker的缓存功能,但这并不是什么大问题)。 更糟糕的是,包含特定项目依赖项的结果图像很大。 在这里,如果我们谈论的是前端项目,我们可以回想起这些项目通常是使用诸如webpack之类的捆绑程序进行处理的,这使得可以方便地将应用程序需要的所有内容打包到销售包中。 结果,不需要用于此类项目的npm软件包文件。 这意味着我们可以在使用相同的Webpack构建项目后摆脱此类文件。

有了这个想法,尝试执行以下操作:

 #   COPY package*.json ./ RUN npm install --production # - COPY . . RUN npm run build:production #    npm- RUN rm -rf node_modules 

但是,这种方法不适合我们。 就像我们已经说过的那样, RUNADDCOPY指令创建了Docker缓存的层,因此我们需要找到一种方法来处理依赖项的安装,构建项目,然后使用单个命令删除不必要的文件。 例如,它可能看起来像这样:

 #      COPY . . #  ,      RUN npm install --production && npm run build:production && rm -rf node_module 

在此示例中,只有一个RUN语句可安装依赖项, node_modules项目并删除node_modules文件夹。 这导致一个事实,即图像的大小将不如包含node_modules文件夹的图像的大小大。 我们仅在项目的构建过程中使用此文件夹中的文件,然后将其删除。 的确,这种方法很糟糕,因为它需要花费很多时间来安装npm依赖项。 您可以使用多阶段图像组装技术来消除此缺陷。

想象一下,我们正在开发一个具有许多依赖项的前端项目,并且我们使用webpack来构建该项目。 通过这种方法,为了减少映像的大小,我们可以利用Docker的功能来进行映像的多阶段组装

 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"] 

通过这种方法,生成的图像比以前的图像小得多,并且我们还使用了node:alpine图像,它本身很小。 这是一对图像的比较,在此过程中可以看出, node:alpine的图像比node:8的图像小得多。


比较来自节点存储库的图像

▍3。 使用Docker缓存


努力使用Docker的缓存功能来构建您的映像。 当使用名称package*.json访问的文件时,我们已经注意了此功能。 这减少了图像的生成时间。 但是,不应轻率利用这一机会。

假设我们在Dockerfile中描述了在从基本Ubuntu:16.04映像创建的映像中安装软件包Ubuntu:16.04

 FROM ubuntu:16.04 RUN apt-get update && apt-get install -y \   curl \   package-1 \   .   . 

当系统处理该文件时,如果安装了很多软件包,则更新和安装操作将花费大量时间。 为了改善这种情况,我们决定利用Docker的层缓存功能并按如下方式重写Dockerfile:

 FROM ubuntu:16.04 RUN apt-get update RUN apt-get install -y \   curl \   package-1 \   .   . 

现在,第一次组装图像时,一切都按预期进行,因为尚未形成缓存。 想象一下,现在我们需要安装另一个软件包package-2 。 为此,我们重写文件:

 FROM ubuntu:16.04 RUN apt-get update RUN apt-get install -y \   curl \   package-1 \   package-2 \   .   . 

由于此命令,将不会安装或更新package-2 。 怎么了 事实是,在执行RUN apt-get update指令时,Docker看不到该指令与之前执行的指令之间的任何区别,因此,它从缓存中获取数据。 并且此数据已经过时。 在处理RUN apt-get install指令RUN apt-get install系统会执行它,因为它看起来不像之前的Dockerfile中的类似指令,但是在安装过程中,可能会发生错误,或者将安装旧版本的软件包。 结果,事实证明, updateinstall命令必须在同一RUN指令中执行,如第一个示例中那样。 缓存是一个很棒的功能,但是鲁ck使用此功能可能会导致问题。

▍4。 减少图像层数


建议尽可能地尽量减少映像层的数量,因为每一层都是Docker映像的文件系统,这意味着映像中的层越小,它将越紧凑。 当使用图像组装的多阶段处理时,实现了图像中的层数的减少和图像尺寸的减小。

总结


在本文中,我们研究了在Docker容器中打包Node.js应用程序并使用此类容器的过程。 此外,我们提出了一些建议,这些建议不仅可以在为Node.js项目创建容器时使用。

亲爱的读者们! 如果您在处理Node.js项目时专业使用Docker,请与初学者分享有关有效使用该系统的建议。

Source: https://habr.com/ru/post/zh-CN440656/


All Articles