长期以来,Docker一直是创建可轻松分发和部署的工件的首选工具。
Docker镜像可以承载几乎所有语言编写的代码;每个主要的操作系统都支持执行Docker镜像的能力,所有的云供应商都至少有一个平台允许部署Docker镜像。
然而,从你的自定义应用程序代码中创建一个Docker镜像需要一点专业知识,尤其是当你在对代码进行修改时定期重建镜像。
每次构建镜像时,很容易不必要地下载数以千计的软件包,浪费了时间,消耗了带宽,也浪费了金钱。
云原生Buildpacks的出现,是利用大型托管服务商在生成和托管Docker镜像方面的十年经验,为构建Docker镜像提供了便利。通过捕捉这些最佳实践,Buildpacks确保你的Docker镜像构建快速而高效。
在这篇文章中,我们将看看Docker镜像如何承载一个简单的Node.js应用程序,回顾一下你可能遇到的一些常见陷阱,并探讨Buildpacks如何让你有效地创建Docker镜像,通常不需要额外的配置。
Node.js Express应用程序的样本
在这篇文章中,我们将建立的样本应用程序是非常初级的。它只是运行Express 应用程序生成器来生成一个显示 "欢迎来到 Express "的网页的最终结果。
虽然很简单,但该示例应用程序展示了如何构建一个承载Node.js应用程序的Docker镜像,以及构建Docker镜像的天真方法可能导致的一些低效率。
教程先决条件
要跟上这个帖子,你需要安装一些工具。
首先,你需要Node.js,Node.js网站提供下载和说明。最新的长期支持(LTS)版本是合适的。
接下来,你将需要Docker,你可以在他们的网站上找到下载和说明。虽然Windows最近获得了原生的容器支持,但这篇文章的重点是构建Linux Docker镜像。
Windows和macOS都通过一个无缝的虚拟机层支持Linux Docker镜像,作为一个开发者,你大多不用考虑这个问题。
最后,为了使用Buildpacks,你必须安装 [pack](https://buildpacks.io/docs/tools/pack/) CLI工具,该工具可从Buildpacks网站获得。
构建一个简单的Docker镜像
构建Docker镜像的传统方法是在一个名为[Dockerfile](https://blog.logrocket.com/reduce-docker-image-sizes-using-multi-stage-builds/#what-are-docker-images).我们可以看到下面一个构建Node.js样本应用程序的例子。
FROM node
WORKDIR /usr/src/app
COPY . .
RUN npm install
EXPOSE 3000
CMD [ "npm", "start" ]
完整的 [Dockerfile](https://docs.docker.com/engine/reference/builder/)完整的 参考信息可以在Docker网站上找到。我们的例子只使用了一小部分可用的命令,但这足以让我们的样本应用Docker化。
FROM 命令定义了你在上面构建的基础镜像。所有主要的工具和编程语言都提供了支持的Docker镜像,开发者可以在此基础上建立自己的镜像,Node.js也不例外。
可用的Node.js Docker镜像的完整列表可以在DockerHub上找到,在这里,使用默认的node 镜像。
FROM node
Docker镜像本质上是一个文件系统,包含运行支持一个应用程序的Linux进程所需的所有文件。在这个文件系统中,我们将创建一个名为/usr/src/app 的目录,以存放我们的Node.js应用程序。
WORKDIR 命令创建了提供的目录,并将其作为任何后续命令的工作目录。
WORKDIR /usr/src/app
然后,你可以通过COPY 命令将Node.js应用程序的源代码从你的本地电脑复制到Docker镜像中。
第一个参数是本地文件的位置。. 表示本地文件在当前工作目录下。
第二个参数是Docker镜像中文件复制到的位置。由于上面的WORKDIR 命令,作为第二个参数传递的. 导致文件被复制到/usr/src/app 。
COPY . .
在这一点上,你可以像在本地一样测试和构建应用程序。在这个简单的例子中,构建应用程序意味着用npm 下载任何依赖项。你可以用RUN 命令在Docker镜像的上下文中运行命令。
RUN npm install
EXPOSE 命令从用生成的Docker镜像创建的容器向Docker主机打开一个端口。示例应用程序默认为3000端口。
EXPOSE 3000
最后,你定义当基于该镜像的容器启动时要运行的命令。在package.json 文件中定义的作为启动Node网络服务器的快捷方式的npm start 命令允许你用CMD 命令运行相同的命令。
CMD [ "npm", "start" ]
为了构建Docker镜像,从存放Dockerfile 文件的目录中运行以下命令。这将指示Docker使用当前目录中的Dockerfile 文件来构建一个名为expressapp 的镜像。
docker build . -t expressapp
一旦镜像构建完成,就可以用该命令运行。
docker run -p 3000:3000 expressapp
这将从名为expressapp 的镜像中创建一个容器,并将本地PC上的3000端口暴露给容器内的3000端口。现在你可以在http://localhost:3000,打开示例应用程序。
现在你已经成功地将样本Node.js应用程序Docker化。然而,上面显示的方法确实有一些明显的缺点。
探索Docker镜像层
在幕后,一个Docker镜像是由多个层组成的。每一层都代表了对镜像的增量变化,各层结合起来就会产生由容器执行的结果文件系统。
Dockerfile 中的每个命令都会创建一个新的层。为了提高构建Docker镜像时的性能,如果Dockerfile 中的指令不改变,复制到镜像中的外部文件也不改变,这些层就会被重复使用。
你可以通过用下面的命令再次重建镜像来看到这一点。
docker build . -t expressapp
注意这次镜像的构建速度远比第一次快。这是因为Dockerfile 文件和复制到镜像中的文件都没有改变,允许Docker重新使用之前生成的层。
现在让我们修改一个文件,强迫Docker重建构成镜像的层。首先,编辑文件views\index.jade ,将欢迎词改为Welcome to Express from Docker 。
extends layout
block content
h1= title
p Welcome to #{title} from Docker
现在,用命令重建镜像。
docker build . -t expressapp
注意,在构建镜像时,Node的依赖关系再次下载。这是因为命令COPY . . ,检测到复制的文件发生了变化,这意味着这个命令生成的前一个图层不能再使用。
这也意味着任何后续的图层都不能被重用,而命令RUN npm install ,被迫再次下载依赖文件。
对于这样一个小的应用程序来说,这是一个小的不便,但更大的Node.js应用程序可能需要在每次改变应用程序源代码时下载数百兆字节的依赖。这不是特别有效,也不是一个可持续的长期方法。
这个问题的典型解决方法是只复制package.json 文件,运行npm install ,然后再复制剩余的应用程序源代码。我们可以看到下面的一个例子。
FROM node
WORKDIR /usr/src/app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
CMD [ "npm", "start" ]
这是一个改进,因为对应用程序源代码的修改不需要再次下载依赖关系。只要package.json 文件不改变,你就可以重新使用由COPY package.json . 和RUN npm install 命令生成的层。
尽管如此,对package.json 文件的任何改变都会导致所有的依赖性重新下载。如果你能在图像构建之间共享node_modules 目录,就像直接从本地电脑构建时在磁盘上保留一样,那不是很好吗?
这就是 Buildpacks 的作用。
用 Buildpacks 生成一个镜像
你可以把Buildpacks看作是由Google、Heroku和Cloud Foundry等公司多年来演化和维护的构建脚本,以最方便和有效的方式将你的应用程序编译成Docker镜像。
为了证明这一点,删除Dockerfile 文件,因为你不再需要它了。Node.js 应用程序现在没有特殊的 Docker 配置文件。
现在,用以下命令构建一个Docker镜像。
pack build expressappbuildpack
如果你第一次运行pack 命令,它会提示你选择一个默认的构建器。
Please select a default builder with:
pack config default-builder <builder-image>
Suggested builders:
Google: gcr.io/buildpacks/builder:v1 Ubuntu 18 base image with buildpacks for .NET, Go, Java, Node.js, and Python
Heroku: heroku/buildpacks:18 Base builder for Heroku-18 stack, based on ubuntu:18.04 base image
Heroku: heroku/buildpacks:20 Base builder for Heroku-20 stack, based on ubuntu:20.04 base image
Paketo Buildpacks: paketobuildpacks/builder:base Ubuntu bionic base image with buildpacks for Java, .NET Core, NodeJS, Go, Ruby, NGINX and Procfile
Paketo Buildpacks: paketobuildpacks/builder:full Ubuntu bionic base image with buildpacks for Java, .NET Core, NodeJS, Go, PHP, Ruby, Apache HTTPD, NGINX and Procfile
Paketo Buildpacks: paketobuildpacks/builder:tiny Tiny base image (bionic build image, distroless-like run image) with buildpacks for Java Native Image and Go
Tip: Learn more about a specific builder with:
pack builder inspect <builder-image>
许多高质量的构建器是由谷歌、Heroku和Paketo等团队提供的。我倾向于坚持使用Heroku提供的构建器,用以下命令进行配置。
pack config default-builder heroku/buildpacks:20
现在,再次运行构建命令。
pack build expressappbuildpack
看着pack 工具检测到应用程序是针对Node.js编写的,安装所有的依赖项,并产生Docker镜像。用这个命令运行新的Docker镜像。
docker run -p 3000:3000 expressappbuildpack
和以前一样,样本应用程序可以从http://localhost:3000。
Buildpacks如何有利于创建Docker镜像
值得花点时间考虑一下你刚刚取得的成果。在没有Docker配置或Dockerfile ,也没有特殊标志或设置来表明你有一个Node.js应用程序的情况下,pack 命令成功地产生了一个嵌入Node.js应用程序的Docker镜像。
更重要的是,Buildpacks在处理应用程序依赖关系的方式上更加智能。为了证明这一点,在package.json 文件的结尾处添加一些空白(比如一个新行),然后保存更改。
以前,对package.json 文件的任何改变,即使是像添加空白这样微不足道的改变,都会导致Docker无法重复使用任何先前生成的层。这反过来又导致了所有的依赖性重新下载。
但是,如果你用pack 重建Docker镜像,现有的依赖关系就会被重新使用。你会看到类似[INFO] Reusing node modules 的日志信息,表明之前下载的依赖项仍然可用。
这是因为Buildpacks巧妙地使用了Docker卷,在不同的构建之间持续保存像依赖性这样的文件,这比依靠层缓存要强得多。
因此,通过一个简单的命令,你可以将你的Node.js应用程序Docker化,而不必担心黑掉一个Dockerfile ,以防止不必要的依赖性下载。
总结
Docker是构建和发布应用程序的一个非常强大的工具,但要充分利用它,开发人员必须对Dockerfile 中的命令与Docker层的关系以及在什么情况下可以和不可以重用层有一个合理详细的了解。
Buildpacks将构建Docker镜像所需的大部分知识抽象化,通过高质量的、经过实战检验的脚本,利用卷等高级功能,快速有效地构建镜像。
Buildpacks使得构建Docker镜像成为可能,只需调用pack 。
这篇文章演示了从自定义的Dockerfile ,手动构建Docker镜像,并探讨了应用程序的依赖性会不必要地重新下载的情况。
然后我们使用pack 命令来构建Docker镜像,展示了即使在通常重建层的情况下,依赖关系也会被重新使用。
The postDockerize Node.js apps with Buildpacksappeared first onLogRocket Blog.