如何在使用yarn工作区的monorepo中从nodejs项目构建docker镜像

52
我们的团队正在研究网站的CI/CD。最近,我们也采用了单一代码库结构,因为这样可以更轻松地管理依赖关系和概览。目前,CI的测试等已准备就绪,但我现在需要进行部署。我想创建所需软件包的Docker镜像。
我考虑的事情:
1)将整个单一代码库拉入Docker项目中,但在我们的项目中运行yarn install会导致总项目大小约为700MB,这主要是由于我们的React Native应用程序造成的,而它根本不应该有Docker镜像。此外,每次部署新版本时都需要长时间的镜像拉取时间。
2)以某种方式捆绑我的项目。对于我们的前端,我们有一个工作设置,所以应该没问题。但我刚刚尝试将webpack添加到我们的express api中,并且由于此问题,在我的bundle内部出现了错误:https://github.com/mapbox/node-pre-gyp/issues/308 3)我尝试仅在所需项目内部运行yarn install,但这仍然会安装所有项目的node_modules。
4)运行npm包:pkg。这会生成一个可在特定系统上以特定节点版本运行的单个文件。这确实有效,但我不确定它如何处理错误和崩溃。

5) 另一个解决方案是将项目从工作区复制出来,在那里运行yarn install。问题在于使用yarn工作区(隐式链接的依赖项)已经消失了。我必须显式地添加我的其他工作区依赖项。一种可能性是从特定的提交哈希引用它们,我现在要测试一下。(编辑:似乎不能将子目录引用为yarn包)

6) ???

我想知道是否有遗漏的选项,可以仅为某个项目保留所需的node_modules,以便我可以保持我的docker镜像较小。


你找到解决方案了吗?我正在做一个类似的项目。 - Peter
如果您将软件包发布到npm上,那么这不会成为问题。在部署过程中,您不应该直接依赖于磁盘上的软件包,而是应该依赖于提交到注册表中的软件包。自动链接工具yarn只应在开发过程中使用。如果您记住这一点,在正常的部署中,您只需将服务目录复制到docker镜像中并在那里安装依赖项,就不会出现任何问题。 - jonathancardoso
4个回答

33

我曾经参与了一个项目,其结构类似于你们的,看起来是这样的:

project
├── package.json
├── packages
│   ├── package1
│   │   ├── package.json
│   │   └── src
│   ├── package2
│   │   ├── package.json
│   │   └── src
│   └── package3
│       ├── package.json
│       └── src
├── services
│   ├── service1
│   │   ├── Dockerfile
│   │   ├── package.json
│   │   └── src
│   └── service2
│       ├── Dockerfile
│       ├── package.json
│       └── src
└── yarn.lock

services/文件夹每个子文件夹包含一个服务。每个服务都是用node.js编写的,有自己的package.json和Dockerfile。它们通常基于Express构建Web服务器或REST API。

packages/文件夹包含所有不属于服务的包,通常是内部库。

一个服务可以依赖于一个或多个包,但不能依赖于另一个服务。一个包可以依赖于另一个包,但不能依赖于一个服务。

主要的package.json(位于项目根目录)只包含一些devDependencies,例如eslint,测试运行程序等。

假设service1依赖于package1package3,则单独的Dockerfile如下:

FROM node:8.12.0-alpine AS base

WORKDIR /project

FROM base AS dependencies

# We only copy the dependencies we need
COPY packages/package1 packages/package1
COPY packages/package3 packages/package3

COPY services/services1 services/services1

# The global package.json only contains build dependencies
COPY package.json .

COPY yarn.lock .

RUN yarn install --production --pure-lockfile --non-interactive --cache-folder ./ycache; rm -rf ./ycache

我使用的实际 Dockerfile 更加复杂,因为它们需要构建子包、运行测试等等。但是你可以通过此示例了解到基本思路。

如您所见,诀窍在于仅复制特定服务所需的软件包。 yarn.lock 文件包含一个 package@version 的列表,其中包含确切版本和已解决的依赖项。 将其复制而不包括所有子包并不是问题,yarn 在安装包含包的依赖项时将使用那里解决的版本。

在您的情况下,React-Native 项目永远不会成为任何 Dockerfile 的一部分,因为它不是任何服务的依赖项,从而节省了大量空间。

为了简洁起见,在答案中省略了很多细节,如果有什么不太清楚的地方,请随时在评论中要求准确性。


4
如果Dockerfile位于service1目录中,那么COPY packages/package1 packages/package1是如何工作的?难道不应该是COPY ../../packages/package1 packages/package1吗? - HenningCash
5
因为我使用了类似于“docker build -f ./services/service1/Dockerfile .”这样的构建命令,将上下文设置为当前目录(在本例中是项目根目录),并使用service1的Dockerfile。 - Anthony Garcia-Labiad
7
我非常希望有一种方法,不需要手动复制软件包,而是让webpack处理安装依赖项。这是否可行? - Travis Tubbs
2
这种方法的缺点是你必须两次定义你的依赖关系;一次在你的服务的 package.json 文件中,另一次在你的 Dockerfile 文件中。 - Nepoxx
你可以使用package.json文件中的信息,在precommit hook/ci中自动生成Dockerfile的部分内容。 - hexagoncode
使用TypeScript会改变什么?当你编译它时,它会创建一个包含JS文件的目录... - Nathan H

1

我在我的项目中有一个与Anthony Garcia-Labiad非常相似的设置,使用skaffold可以将它全部运行起来。这使我能够指定上下文和Docker文件,类似于这样:

apiVersion: skaffold/v2beta22
kind: Config
metadata:
  name: project
deploy:
  kubectl:
    manifests:
      - infra/k8s/*
build:
  local:
    push: false
  artifacts:
    - image: project/service1
      context: services
      sync:
        manual:
          - src: "services/service1/src/**/*.(ts|js)"
            dest: "./services/service1"
          - src: "packages/package1/**/*.(ts|js)"
            dest: "./packages/package1"
      docker:
        dockerfile: "services/service1/Dockerfile"

1
经过多次尝试,我发现小心使用.dockerignore文件是控制最终镜像的好方法。当在monorepo下运行以排除“其他”软件包时,这非常有效。
对于每个软件包,我们都有一个类似命名的dockerignore文件,在构建之前替换实际的.dockerignore文件。
例如, cp admin.dockerignore .dockerignore 以下是admin.dockerignore的示例。请注意该文件顶部的*表示“忽略所有内容”。前缀!表示“不要忽略”,即保留。组合意味着忽略除指定文件外的所有内容。
*
# Build specific keep
!packages/admin

# Common Keep
!*.json
!yarn.lock
!.yarnrc
!packages/common

**/.circleci
**/.editorconfig
**/.dockerignore
**/.git
**/.DS_Store
**/.vscode
**/node_modules

0

我们最近将我们的后端服务放入了一个单一代码仓库,这是我们需要解决的几个问题之一。Yarn在这方面没有任何帮助,所以我们不得不寻找其他方法。

首先,我们尝试了@zeit/ncc,虽然有些问题,但最终我们成功地获得了最终的构建版本。它生成了一个包含所有代码和依赖项代码的大文件。看起来很好。我只需要将几个文件(js、源映射、静态资源)复制到Docker镜像中。镜像要小得多,应用程序也能正常工作。但是运行时内存消耗大大增加,容器的运行内存从 ~70MB 增加到了 ~250MB。不确定我们是否做错了什么,但我还没有找到任何解决方案,只有一个issue提到了这个问题。我猜测Node.js在加载时会解析并加载捆绑包中的所有代码,即使其中大部分永远不会被使用。

我们所需要的就是将每个软件包的生产依赖项分开构建成一个精简的Docker镜像。这似乎并不那么简单,但我们终究找到了一种工具。

我们现在正在使用fleggal/monopack。它将我们的代码与Webpack捆绑在一起,并通过Babel进行转译。因此,它也会生成一个文件包,但它不包含所有依赖项,只有我们的代码。这一步是我们不真正需要的,但我们不介意它存在。对我们来说,重要的部分是 - Monopack仅将软件包的生产依赖树复制到dist/bundled node_modules中。这正是我们所需要的。Docker镜像现在只有100MB-150MB,而不是700MB。

还有一种更简单的方法。如果您的node_modules中只有几个非常大的npm模块,则可以在根package.json中使用nohoist。这样,yarn会将这些模块保留在软件包的本地node_modules中,而不必将其复制到所有其他服务的Docker镜像中。

eg.:

"nohoist": [
  "**/puppeteer",
  "**/puppeteer/**",
  "**/aws-sdk",
  "**/aws-sdk/**"
]

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