Dockerfile应该执行“npm install”和“npm run build”,还是只需要复制这些文件?

26

我是一个对Docker有些陌生的人,正在尝试理解一些概念。

在很多教程和文章中(实际上,几乎所有的教程和文章),这是一个典型的create-react-app和nginx配置Dockerfile:

# CRA
FROM node:alpine as build-deps
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm install
COPY . ./
RUN npm run build

# Nginx
FROM nginx:1.12-alpine
COPY --from=build-deps /usr/src/app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

假设一切都按预期运行,图像将会非常大。
我有一个稍微不同的想法。在本地运行 "npm install && npm run build",然后使用以下 Dockerfile:
FROM nginx:1.12-alpine
WORKDIR /opt/app-root

COPY ./nginx/nginx.conf /etc/nginx/
COPY ./build ./src/dist/

COPY ./node_modules .

USER 1001
EXPOSE 8080
ENTRYPOINT ["nginx", "-g", "daemon off;"]

哪种方法更好?每当我运行docker build -t app-test:0.0.1 .时,我觉得第二种方法总是更快。

5个回答

11
在容器内构建可以保证构建结果的可预测性和可复现性。例如在 macOS 和 Linux 上运行 npm install 可能会产生不同的 node_modules,比如 node-gyp

如果正在构建的容器不是一个 Node.js 应用程序,则通常使用多阶段构建来构建 node_modules。也就是说,您的实际 nginx 应用程序本身并不依赖于 Node.js,而是依赖于其包含的 node_modules 目录及其文件。因此,我们在 Node 容器中生成 node_modules,然后将其复制到新容器(nginx)中。

因此,使用多阶段 Dockerfile 进行构建的所有人将生成完全相同的容器。如果在构建过程中将本地的 node_modules 复制到容器中,则其他同事将无法预测 node_modules 的内容。


6

Dockerfile应该执行“npm install”和“npm run build”,还是仅复制这些文件?

TL;DR:它应该在多阶段镜像的“构建步骤”中始终执行所有必要的构建命令!

长答案:

在您发布的第一个“教程” Dockerfile示例中,使用了多阶段构建。使用多阶段构建,您可以丢弃在前几个阶段创建的工件,并仅保留那些您真正需要的文件和更改。在这种情况下,安装的“dev”软件包不会被复制到最终映像中,因此不会占用任何空间。构建文件夹将仅包含在运行时所需的代码和节点模块,而没有任何在构建的第一步中需要的开发依赖项,以编译项目。

在第二种方法中,您在Dockerfile外部运行npm install && npm run build,然后将结果复制到最终镜像中。虽然这样可以工作,但从devops的角度来看,这不是一个好主意,因为您希望将所有必需的构建说明一致地放在一个地方(最好是一个Dockerfile中),以便下一个构建您的镜像的人不必弄清楚编译过程的工作原理。从本地机器复制构建结果的另一个问题是,您可能正在运行具有不同节点版本等的另一个操作系统,这可能会影响构建结果。如果您像“教程”Dockerfile一样在Dockerfile中进行构建,则可以完全控制OS和环境(节点版本、节点Sass库等),并且执行docker build的每个人都将获得相同的编译结果(假设您确定了Dockerfile基础映像的节点版本,即使用FROM node:14.15.4-alpine as build-deps而不仅是FROM node:alpine as build-deps)。
关于Dockerfile演进的最后一点说明。过去,实际上是在Dockerfile之外(或在另一个单独的Dockerfile中)执行编译,然后将所有结果复制到最终镜像中的方法。这与您的OP中提到的第二种方法相符。但是,鉴于上述所有缺点,Docker架构师在2017年发明了多阶段构建。以下是来自 docker 博客的一些启示性语录:
“在多阶段构建之前,Docker用户会使用脚本在主机上编译应用程序,然后使用Dockerfile构建映像。然而,多阶段构建可以更容易地创建小型且效率显著提高的容器,因为最终映像可以不包含任何构建工具。此外,不再需要外部脚本来协调构建。” 官方文档中也重申了同样的想法:
实际上,使用一个Dockerfile进行开发(其中包含构建应用程序所需的所有内容),并使用一个精简版本进行生产部署(仅包含应用程序和运行它所需的内容)是非常常见的。这被称为“构建器模式”。维护两个Dockerfile并不理想。[...] 多阶段构建大大简化了这种情况![...] 您只需要单个Dockerfile,也不需要单独的构建脚本,只需运行docker build即可。最终结果与以前相同,都是一个小型的生产镜像,但复杂性显著降低。您无需创建任何中间镜像,也无需将任何文件提取到本地系统中。

你是在指第一个 Dockerfile 吗?我需要将 node_modulesbuild/ 目录复制到容器镜像中,否则应用程序将无法正常工作。我有点困惑你在建议什么。 - user14697413
我认为它们占用了相同的空间。如果我错了,有人可以纠正我,但在第一个示例中,您正在下载node_modules并构建捆绑包 - 在第二个示例中,您只需复制node_modules和构建的捆绑包。在我看来,我认为两个图像都占用了相同的空间。我仅从问题的角度来说话,而不是从devops的角度,因为@B12Toaster是指这一点。 - Mike K
抱歉,可能有些混淆了,我实际上是在指"dev dependencies",而不是那些在运行时需要的node_modules。例如,typescript的node模块不会出现在最终的构建文件夹中,因此也不会被复制到第二阶段。我已经更新了我的回答。 - Felix K.
我倾向于不同意。你正在寻找可能的最小图像。如果Dockerfile也在生产中使用,你不能/不应该有多阶段构建。我认为第二种方法更好。在本地构建和准备所有内容,然后在Dockerfile中使用COPY将其复制到nginx上进行服务。 - Mike K
1
嗨@MikeK,实际上多阶段构建是为了生成没有本地构建脚本的精简生产镜像而开发的。引用文档中的话:“您只需要单个Dockerfile。您也不需要单独的构建脚本。最终结果与以前一样小的生产镜像,但复杂度显著降低。您不需要创建任何中间镜像,也不需要将任何工件提取到本地系统。” - Felix K.

3
为此,您可以使用多阶段docker构建。
在第一个容器中,您将安装所有依赖项(包括开发依赖项),然后运行npm run build。它将构建您的应用程序,但是您将在node_modules中拥有无用的dev依赖项。您不必复制该node_modules。
在第二个容器中,您将运行npm install --production --no-audit,并从第一个容器中复制dist目录。现在,您将拥有已编译的代码和仅包含生产模块的node_modules文件夹。
这将使其更轻便,但构建时间会稍长。

2

从容器化的角度来看,我认为第一种选择更好,因为您不需要在笔记本电脑上安装软件包(npm)即可运行应用程序。您只需要安装Docker即可。


1
如果您不需要 node_modules 目录树(对于 Nginx 托管的浏览器应用程序而言,您不需要),那么仅复制构建后的应用程序的第二种方法就可以了。
有几个原因特别想要第一种方法,在 Docker 中运行构建。如果您的构建中有架构特定的细节(例如带有本机扩展的 Node 包),Docker 可能是与主机系统不同的操作系统和库堆栈,因此您可能无法直接复制 node_modules 目录。如果您的构建真的很特定,只是在语言运行时进行微小修复,您可以在 Dockerfile 中强制使用非常特定版本的 Node。
我所工作过的几乎所有浏览器应用程序都可以使用我手边的任何 node 二进制文件进行构建,一旦完成,dist 目录树就是平台无关的静态文件。
“正常”的样式在不同的语言中似乎有所不同。特别是Java应用程序似乎通常在Docker外部构建应用程序,然后将(平台无关的)最终.jar文件复制到镜像中。Go倾向于使用多阶段构建,在极小的最终镜像中复制已构建的二进制文件。如果我要编写一个Node浏览器应用程序,它可能会像您的第一个表单一样,即在Docker中运行RUN yarn build,但我已经看到了很多变化。”

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接