如何在具有纱线工作空间的 monorepo 中从 nodejs 项目构建 docker 映像

Posted

技术标签:

【中文标题】如何在具有纱线工作空间的 monorepo 中从 nodejs 项目构建 docker 映像【英文标题】:How to build a docker image from a nodejs project in a monorepo with yarn workspaces 【发布时间】:2018-10-17 08:08:48 【问题描述】:

我们目前正在与我们的团队一起为我们的网站研究 CI/CD。我们最近还适应了 monorepo 结构,因为这使我们的依赖关系和概览变得更加容易。目前测试等已为 CI 做好准备,但我现在正在部署。我想创建所需包的 docker 映像。

我考虑的事情:

1) 将完整的 monorepo 拉入 docker 项目,但在我们的项目中运行 yarn install 会导致项目总大小约为 700MB,这主要是由于我们的 react native 应用程序甚至不应该有 docker 映像。此外,每次我们必须部署新版本时,这都会导致很长的图像拉取时间

2) 以某种方式捆绑我的项目。使用我们的前端,我们有工作设置,所以应该没问题。但我只是尝试将 webpack 添加到我们的 由于这个问题,express api 并在我的包中出现错误:https://github.com/mapbox/node-pre-gyp/issues/308

3) 我尝试只在需要的项目中运行 yarn install,但这仍然会为我的所有项目安装我的 node_modules。

4) 运行 npm 包:pkg。这导致单个文件准备好在具有特定节点版本的特定系统上运行。这确实有效,但我不确定这将如何处理错误和崩溃。

5) 另一种解决方案可能是将项目复制到工作区之外并在那里运行纱线安装。这样做的问题是纱线工作空间(隐式链接依赖项)的使用已经过去了。我必须明确添加我的其他工作区依赖项。一种可能性是从某个提交哈希中引用它们,我现在将对其进行测试。 (编辑:您似乎不能将子目录作为纱线包引用)

6) ???

我想知道我是否缺少一个选项,即只为某个项目提供所需的 node_modules,这样我就可以保持我的 docker 镜像很小。

【问题讨论】:

您找到解决方案了吗?我正在做一个类似的项目。 如果你将你的包发布到npm,这不会是一个问题,你不应该在部署过程中直接依赖磁盘中的包,而应该依赖提交到注册表的那个。自动连接纱线只应在开发过程中使用。如果您牢记这一点,那么您只需将服务目录复制到 docker 映像并在那里安装 deps 即可正常部署。 【参考方案1】:

我从事过一个与您的结构相似的项目,它看起来像:

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,test runner等。

单个Dockerfile 看起来像这样,假设service1 依赖于package1package3

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

我使用的实际Dockerfiles 更复杂,因为他们必须构建子包、运行测试等。但是您应该通过这个示例了解想法。

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

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

为了简洁起见,我在那个答案中省略了很多细节,如果有什么不是很清楚,请随时在评论中要求准确。

【讨论】:

如果 Dockerfile 位于 service1 目录中,COPY packages/package1 packages/package1 如何工作?不是COPY ../../packages/package1 packages/package1吗? 这是因为我使用了诸如 docker build -f ./services/service1/Dockerfile . 之类的构建命令,它使用 service1 的 Dockerfile 将上下文设置为当前目录(在本例中为项目根目录)。 我真的希望有一种方法不必复制包,只需让 webpack 处理安装依赖项。这可能吗? 这种方法的缺点是你必须定义你的依赖两次;一次在您的服务的package.json 和一次在您的Dockerfile 您可以使用来自package.json 文件的信息在 precommit hook/ci 中自动生成Dockerfiles 的部分内容。【参考方案2】:

经过大量试验和错误后,我发现谨慎使用文件.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

【讨论】:

【参考方案3】:

我们最近将后端服务放到了一个 monorepo 中,这是我们必须解决的几个问题之一。 Yarn 在这方面没有任何帮助我们的东西,所以我们不得不寻找其他地方。

首先我们尝试了@zeit/ncc,出现了一些问题,但最终我们设法获得了最终版本。它会生成一个包含所有代码以及所有依赖项代码的大文件。看起来很棒。我只需要将几个文件(js、源映射、静态资产)复制到 docker 映像。图像要小得多,并且该应用程序可以正常工作。但是运行时内存消耗增加了很多。正在运行的容器消耗了 ~250MB 而不是 ~70MB。不确定我们是否做错了什么,但我没有找到任何解决方案,而且只有一个 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 映像中。

例如:

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

【讨论】:

【参考方案4】:

我在我的项目中有一个与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"

【讨论】:

以上是关于如何在具有纱线工作空间的 monorepo 中从 nodejs 项目构建 docker 映像的主要内容,如果未能解决你的问题,请参考以下文章

无法在纱线工作区 monorepo 中添加包

使用纱线工作区在monorepo中跨项目共享配置变量?

React Monorepo 纱线工作区 + 打字稿 + 绝对导入

如何从纱线工作区中删除 CRA?

强制纱线将某些模块安装到给定的工作区,而不是将它们移动到monorepo的共享节点模块?

纱线工作区没有建立本地依赖