Docker 更新 gem 时捆绑安装缓存问题

Docker bundle install cache issues when updating gems

我在开发和生产中都使用 docker,真正困扰我的一件事是 docker 缓存简单性。我有 ruby 应用程序,它需要 bundle install 来安装依赖项,所以我从以下 Dockerfile 开始: ADD Gemfile Gemfile ADD Gemfile.lock Gemfile.lock RUN bundle install --path /root/bundle 所有依赖项都被缓存,并且在我添加新的 gem 之前效果很好。即使我添加的 gem 只有 0.5 MB,从头开始安装所有应用程序 gem 仍然需要 10-15 分钟。由于依赖项文件夹的大小(大约 300MB),然后再花 10 分钟来部署它。

我遇到了 node_modules 和 npm 完全相同的问题。我想知道,有没有人找到解决这个问题的方法?

我目前的研究成果:

我找到了两个使用外部数据卷进行 gem 存储的可能解决方案:one and two

简而言之,

  • 您指定的图像仅用于存储 gem
  • 在您的 app 图像中,在 docker-compose.yml 中您通过 volumes_from.
  • 指定了 BUNDLE_PATH 的挂载点
  • 当您的应用程序容器启动时,它会执行 bundle check || bundle install,一切顺利。

这是一种可能的解决方案,但对我来说,它感觉有点违背 docker 的方式。具体来说,bundle install 对我来说听起来应该是构建过程的一部分,而不应该是运行时的一部分。其他依赖于 bundle install 的东西,比如 asset:precompile 现在也是一个运行时任务。

这是一个可行的解决方案,但我期待更强大的解决方案。

我将 gem 缓存到应用程序 tmp 目录中的 tar 文件中。然后,在进行捆绑安装之前,我使用 ADD 命令将 gem 复制到一个层中。来自我的 Dockerfile.yml:

WORKDIR /home/app

# restore the gem cache. This only runs when
# gemcache.tar.bz2 changes, so usually it takes
# no time
ADD tmp/gemcache.tar.bz2 /var/lib/gems/

COPY Gemfile /home/app/Gemfile
COPY Gemfile.lock /home/app/Gemfile.lock
RUN gem update --system && \
gem update bundler && \
bundle install --jobs 4 --retry 5

确保将 gem 缓存发送到 docker 机器。我的 gem 缓存是 118MB,但由于我是在本地构建,所以它的复制速度很快。我的 .dockerignore:

tmp
!tmp/gemcache.tar.bz2

您需要从构建的图像中缓存 gems,但最初您可能没有图像。像这样创建一个空缓存(我在 rake 任务中有这个):

task :clear_cache do
  sh "tar -jcf tmp/gemcache.tar.bz2 -T /dev/null"
end

构建图像后,将 gem 复制到 gem 缓存。我的图像被标记为 app。我从图像创建一个 docker 容器,使用 docker cp 命令将 /var/lib/gems/2.2.0 复制到我的 gem 缓存中,然后删除该容器。这是我的抽佣任务:

task :cache_gems do
  id = `docker create app`.strip
  begin
    sh "docker cp #{id}:/var/lib/gems/2.2.0/ - | bzip2 > tmp/gemcache.tar.bz2"
  ensure
    sh "docker rm -v #{id}"
  end
end

在随后的图像构建中,gem缓存在调用 bundle install 之前被复制到一个层。这需要一些时间,但比从头开始 bundle install 快。

之后的构建速度更快,因为 docker 已经缓存了 ADD tmp/gemcache.tar.bz2 /var/lib/gems/ 层。如果对 Gemfile.lock 进行了任何更改,则只会构建这些更改。

没有理由在每次 Gemfile.lock 更改时重建 gem 缓存。一旦缓存和 Gemfile.lock 之间存在足够的差异,导致 bundle install 变慢,您就可以重建 gem 缓存。当我确实想要重建 gem 缓存时,它是一个简单的 rake cache_gems 命令。

"copy local dependencies" 方法(接受的答案)在我看来是个坏主意。对您的环境进行 docker 化的全部意义在于拥有一个隔离的、可重现的环境。

这里是how we are doing it

# .docker/docker-compose.dev.yml
version: '3.7'
services:

  web:
    build: .
    command: 'bash -c "wait-for-it cache:1337 && bin/rails server"'
    depends_on:
      - cache
    volumes:
      - cache:/bundle
    environment:
      BUNDLE_PATH: '/bundle'

  cache:
    build:
      context: ../
      dockerfile: .docker/cache.Dockerfile
    volumes:
      - bundle:/bundle
    environment:
      BUNDLE_PATH: '/bundle'
    ports:
      - "1337:1337"

volumes:
  cache:
# .docker/cache.Dockerfile
FROM ruby:2.6.3
RUN apt-get update -qq && apt-get install -y netcat-openbsd
COPY Gemfile* ./
COPY .docker/cache-entrypoint.sh ./
RUN chmod +x cache-entrypoint.sh
ENTRYPOINT ./cache-entrypoint.sh
# .docker/cache-entrypoint.sh
#!/bin/bash

bundle check || bundle install
nc -l -k -p 1337
# web.dev.Dockerfile
FROM ruby:2.6.3
RUN apt-get update -qq && apt-get install -y nodejs wait-for-it
WORKDIR ${GITHUB_WORKSPACE:-/app}
# Note: bundle install step removed
COPY . ./

这类似于@EightyEight 解释的概念,但它没有将 bundle install 放入主服务的启动中,相反,更新由不同的服务管理。无论哪种方式,都不要在生产中使用这种方法。 运行 在构建步骤中没有安装其依赖项的服务至少会导致不必要的停机时间。