【问题标题】:Installing gems in Dockerfile without Gemfile在没有 Gemfile 的情况下在 Dockerfile 中安装 gem
【发布时间】:2020-04-10 23:05:30
【问题描述】:

我的团队使用 Dockerfile 托管我们的 Rails 应用程序。我们有一些缓慢的宝石确实会减慢我们的构建速度。 (我在看你grpc。????)

是否可以在将 Gemfile 复制到我们的 Dockerfile 之前安装一些 gem?这将允许 Docker 缓存这些构建步骤,因此我们不必在每次 Gemfile 更改时重新安装缓慢的 gem。

我试过了,但bundle install 仍在安装grpc、sassc 和nokogiri。

RUN gem install grpc --version 1.28.0
RUN gem install sassc --version 2.2.1
RUN gem install nokogiri --version 1.10.9

WORKDIR /app

ADD Gemfile Gemfile.lock .ruby-version /app/
RUN bundle install

【问题讨论】:

  • bundle install --deployment from bundler.io/guides/deploying.html#manual-deployment 应该让您走上正轨。本质上,您以这种方式编译 gems,然后将它们添加到 vendor/bundle,并且不必每次都重新编译。
  • 为什么您认为在bundle install 命令之外安装它们会节省您的时间?同样使用 --deployment 和 vendor 文件夹也无济于事 - docker 层缓存应该已经处理好它,并且不应该运行这个 bundler install 层,除非 Gemfile 中有任何更改。
  • @DannyB Docker 缓存其安装的每一层。如果我可以在引入 Gemfile 之前安装它们,那么该图像层将在后续构建中缓存。
  • @anothermh 感谢您的回复!我不太确定这能解决我的问题。特别慢的 gem 需要在本地编译,而且您的解决方案似乎仍然需要 Gemfile。
  • 我错过了标题的“没有 Gemfile”部分,因为在 Dockerfile 中指定了 Gemfile。现在我很困惑。但解决方案似乎是跳过你的RUN gem 指令,使用ADD Gemfile Gemfile.lock,添加RUN bundle install,然后 执行ADD /app/。

标签: ruby-on-rails ruby docker bundle bundler


【解决方案1】:

您可以考虑拆分gemfile,考虑以下文件。

缓慢的宝石

ruby File.read('.ruby-version').strip

gem 'rubocop'

宝石文件

ruby File.read('.ruby-version').strip

# add the "slow" gems to the gem-bundle so we do not have to redefine them.
instance_eval File.read('slow-gems')

gem 'flay'

dockerfile

WORKDIR /app

ADD slow-gems slow-gems.lock .ruby-version /app/
RUN bundle install --gemfile=slow-gems

ADD Gemfile Gemfile.lock /app/
RUN bundle install

这还可以防止您重新定义 docker 映像和 gemfile 中的所有 gem。您可能会遇到的唯一问题是两个锁定文件中的版本会分开。目前,我还没有解决方案,但是在使用当前将它们添加到 dockerfile 的方法时也会发生这种情况。

第二个bundle install 将重新使用slow-gems 文件中已安装的gem,这需要不到一秒钟的时间。

附加: 不要忘记使用 docker 的内置缓存,否则这不会更快,并且对您没有帮助。

【讨论】:

  • 我真的很喜欢这个解决方案。明天我将对其进行测试,看看它是否有效,但似乎这可能适合我。 ?谢谢!
【解决方案2】:

问题:

所以让我们先了解一下这个问题。每当您尝试启动应用程序时,都需要花费大量时间。我想我非常了解您的问题。

  1. 从网上下载 gem
  2. 安装和启动应用程序

可能的解决方案:

这是docker文件的表现。我们有 2 个领域正在产生问题,以及 2 个不同的解决方案

解决方案:

  • 在本地下载 gem 文件并放在一个文件夹中,然后在 dockerfile 中将这些 gem 文件复制到容器中,然后安装。

  • 为您的应用程序创建基础映像。从Ruby/您喜欢的任何其他人创建一个基础映像,并使用上述方法安装您的gems:

    1. 从本地文件夹复制 gems,然后粘贴到container 或
    2. 使用RUN 命令安装gem。随你喜欢。

这将是一个一次性的过程。有了这个,您可以安装已经包含耗时的 gem 的基本映像。现在在你的应用程序dockerfile(负责启动应用程序)中,你只需要使用你自己创建的基础镜像而不是Ruby 或Linux from Docker Hub

我们将了解如何构建您自己的基本预配置映像。
让我们一步一步来看看。

让我们关注这个 GitHub 代码库:https://github.com/dupinder/docker-ruby-gem-game

文件夹结构(这将有助于理解本文)

  • 应用程序/dockerfile
  • gems/下载并放置本地 gems
  • 码头文件

  • 创建基础映像并在 docker 容器中安装本地 gem。

码头文件


    FROM ruby:latest
    RUN mkdir -p /gems 
    COPY /gems/grpc-1.28.0-universal-darwin.gem /gems/grpc-1.28.0-universal-darwin.gem
    COPY /gems/sassc-2.2.1.gem /gems/sassc-2.2.1.gem
    WORKDIR /gems
    RUN gem install --force --local *.gem

使用以下命令从此 dockerfile 构建映像

docker build --rm -f "dockerfile" -t ruby-gem-base-image:latest "."

  • 第 1 步: 我使用基础镜像作为 ruby​​,如果你使用 Linux,你可以随心所欲,然后在下一步中你需要安装 Ruby

  • 第 2 步: 在容器中创建文件夹名称 gems。

  • 第 3 步和第 4 步: 将需要安装在容器内的 Gem 从本地目录复制到 dockercontainer 内的目录。
  • 第 5 步和第 6 步: 将工作目录更改为 gems,因为我们要将 gems 安装在此目录中,所以下一个命令是 gem install --force --local *.gem,它有助于在其中安装 gems本地目录。

这样,我们解决了 50% 的时间消耗。现在 docker 将永远不会每次都从 Internet 下载 gems 并安装。

现在让我们检查一下是否安装了我们所需的任何 gem。为此:

  • 运行命令docker images我们将拥有我们新构建的图像ruby-gem-base-image

  • 以分离模式运行容器,以便我们以后可以execdocker run -it -d ruby-gem-base-image

  • 运行docker ps获取容器ID。

  • 在容器docker exec -it d28234630343 bash 中以bash 的身份运行exec。
  • 运行gem list 这将打印已安装的gem 列表,您将在那里看到所需的gem。

查看您的 gem 是从本地目录安装的。


如果您按照这些步骤操作,您的问题就会得到解决。但是现在你需要如果在启动你的应用程序 docker 容器之前已经安装了 gems。

对于这个问题,我们可以使用ruby-gem-base-image 图像作为我们的ruby 应用程序基础图像。如果您还记得 GitHub 存储库,我们有一个应用程序目录,如果我们看到它有一个 dockerfile。

FROM ruby-gem-base-image:latest
CMD ["gem", "list"]

这是您想要部署应用程序时可以使用的 dockerfile,使用具有您的 gem 的预构建 docker 映像。 我写了任务gem list 来检查这个容器是否有来自父图像的宝石。

我认为这有点清楚。您的问题将由此得到解决。

如果您需要任何其他帮助或需要帮助来了解此过程,请询问。

---------杜宾德。

【讨论】:

  • 感谢 Dupinder 的详细解答。但是,我不确定这是否能满足我的需求。我试图阻止的缓慢的事情是构建 docker 映像所需的时间。每次我更改 Dockerfile 时,都必须重新安装 all 的 gem。对于选项 1,我无法在本地安装和构建 gem,然后将它们复制到 Dockerfile 中,因为安装缓慢的 gem 是必须从源代码编译的。我运行的是 macOS,但 Ruby Docker 镜像在 Linux 中,所以复制编译的镜像是不行的。
  • 对于选项 1,我可以使用基础映像,但 Docker 会为我缓存构建过程的每个步骤。如果我可以将慢速 gem 的安装与快速 gem 的安装分开,先安装慢速 gem,那么 Docker 会在我下次运行时缓存该步骤。在上面的选项 2 中,您似乎正在使用 gem install 安装 gem,但我还没有找到阻止 bundle install 重新安装相同 gem 的方法。我错过了什么吗?
  • 我们应该明白 dockerfile 中的每个命令都是在一个容器中运行的,该容器是由先前命令创建的映像产生的。图像一层一层叠加。因此,您可以做两件事: 1. 查看 dockerfile 中发生更改的位置。例如,如果您的 dockerfile 中有 10 行,并且每次都在第 2 行发生更改,那么每次都需要构建该更改行之后的 8 层。您可以评估是否可以将第 2 行移到文件后面的某个位置(这可能会也可能不会)。
  • 2.查看创建中间图像和多个 dockerfile(或使用多阶段构建功能)。因此,例如,如果您构建的第一部分采用基本操作系统映像并安装几个先决条件,然后将您的应用程序分层,您可能会创建具有先决条件的映像并在没有先决条件的情况下重新使用它为应用部分重建所有层。
【解决方案3】:

您可以使用选项 bundle install --deployment 或 --path 指定可以安装 gem 的文件夹(仅为此目的构建容器,然后将文件夹复制到 docker 外部)。然后将此目录映射为容器中的一个卷,在映射完成后运行 bundle install ...或者简单地复制文件夹:

# outside actual image

bundle install --path=vendor/cache

# actual docker image
COPY . /app
COPY vendor/cache /app/vendor/cache
COPY .bundle /app/.bundle 

WORKDIR /app

RUN bundle install --path=vendor/cache

带有 --path=vendor/cache 的命令会生成一个包配置,就像这个:

---
BUNDLE_DEPLOYMENT: "true"
BUNDLE_PATH: "vendor/cache"

只要您的已编译扩展 gem 版本在您的 Gemfile 上没有更改,就不应再次安装它们。

为了不将供应商/缓存文件夹提交到源文件,您可以将其托管在某个地方并让开发人员下载它。我认为你可以把这个文件夹放在哪里,以及如何将它复制到图像中,这是一个相当开放的问题,我需要更多关于你的设置的信息来推荐一些东西。您究竟在哪里部署您的应用?

示例:

带有本机扩展的 AWS lambda gem 存在一个问题 - 您可以将它们安装在您的操作系统系统中,但它们与 lambda 环境不兼容。

制作有效的 gem 包的方法是执行以下操作:

1 - 使用容器来安装带有原生扩展的 gems,以与 lambda 兼容的方式:

docker run --rm -v "$PWD":/var/task lambci/lambda:build-ruby2.7 bundle install --deployment

2 - 然后,在另一个容器上,您将 GEMS 映射为卷或复制它们,然后运行您的应用程序

docker run --rm --env-file=.env -v $PWD:/var/task:ro,delegated lambci/lambda:ruby2.7 send.lambda_handler

我在这里建议您对您的应用程序执行类似的操作 - 使用一个图像来构建您的平台 gem,然后使用另一个图像来运行应用程序,将这些 gem 传递给它。 只要这两个图像是相同的操作系统,gem 就可以兼容。

我的团队在非 docker 部署管道上使用了与此类似的设置,我们在推送新版本之前将 gem 预安装在文件夹中,并设置捆绑程序以从中获取 gem。

【讨论】:

  • sassc 和 nokogiri 由于原生扩展而运行缓慢。如果我使用(例如)mac 和alpine-image,本地构建它们不会引起问题吗?因为这应该在我的 mac 构建扩展上使用,可能不适用于alpine-image。
  • 使用 alpine-image 创建一个容器,其唯一目的是安装 gem。然后将gem文件夹复制到本地。这样,宝石就可以兼容(但不兼容 mac,可能是 ^^;)。当我说build a container only for this purpose and then copy the folder outside docker 时,我认为这是不言而喻的。我使用相同的方法使用github.com/lambci/docker-lambda 为 AWS Lambda 构建 nokogiri(安装在我的机器上的 gem 在那里不起作用 - 就像 mac 示例一样。但是安装在这些图像中的那些起作用)
  • @cesartalves 我相信丹尼斯是对的,他认为从一个操作系统(如 macOS)复制构建的图像到另一个操作系统(如 Linux)是行不通的。我的团队使用多种操作系统,因此我们需要做一些适合所有人的事情。
  • 您的 gem 将安装在 docker 容器中,因此如果它们使用的应用程序的 docker 映像相同,它们将可以工作!我假设每个人都在容器内使用您的应用程序。我可能不知道如何让自己更清楚:您会将 gem 安装在一个容器中,然后您不会在您的操作系统中使用它们,而是在另一个容器中使用与它们创建时相同的基本映像,根据定义兼容。
  • 我已将我最近处理的 AWS lambda 示例更详细地添加到我的答案中。它解决了一个不同的问题,但我相信这个想法可以用来解决你的问题
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-23
相关资源
最近更新 更多