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 完全相同的问题。我想知道,有没有人找到解决这个问题的方法?
我目前的研究成果:
Source to image - 在增量构建中缓存任意文件。不幸的是,由于它的工作方式,即使 gems 没有改变,它也需要将整个 300MB 推送到注册表。更快的构建 -> 更慢的部署,即使 gems 没有更新。
Gemfile.tip - 将 Gemfile 分成两个不同的文件,并且只向其中一个添加 gems。非常具体的捆绑器解决方案,我不相信它会扩展到超过添加 1-2 gems。
Harpoon - 如果不是他们强制放弃 Dockerfile 并切换到他们自己的格式,那将是一个很好的选择。这对团队中的所有新开发人员来说意味着额外的痛苦,因为此工具集需要时间从 docker.
中单独学习
暂时包缓存。那只是我不确定是否可能的想法。在安装包之前以某种方式将包管理器缓存(不是依赖项文件夹)带到机器上,然后将其删除。根据我的 hack,它显着加快了 bundler 和 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 化的全部意义在于拥有一个隔离的、可重现的环境。
# .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
放入主服务的启动中,相反,更新由不同的服务管理。无论哪种方式,都不要在生产中使用这种方法。 运行 在构建步骤中没有安装其依赖项的服务至少会导致不必要的停机时间。
我在开发和生产中都使用 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 完全相同的问题。我想知道,有没有人找到解决这个问题的方法?
我目前的研究成果:
Source to image - 在增量构建中缓存任意文件。不幸的是,由于它的工作方式,即使 gems 没有改变,它也需要将整个 300MB 推送到注册表。更快的构建 -> 更慢的部署,即使 gems 没有更新。
Gemfile.tip - 将 Gemfile 分成两个不同的文件,并且只向其中一个添加 gems。非常具体的捆绑器解决方案,我不相信它会扩展到超过添加 1-2 gems。
Harpoon - 如果不是他们强制放弃 Dockerfile 并切换到他们自己的格式,那将是一个很好的选择。这对团队中的所有新开发人员来说意味着额外的痛苦,因为此工具集需要时间从 docker.
中单独学习
暂时包缓存。那只是我不确定是否可能的想法。在安装包之前以某种方式将包管理器缓存(不是依赖项文件夹)带到机器上,然后将其删除。根据我的 hack,它显着加快了 bundler 和 npm 的软件包安装速度,而不会因不必要的缓存文件而使机器膨胀。
我找到了两个使用外部数据卷进行 gem 存储的可能解决方案:one and two。
简而言之,
- 您指定的图像仅用于存储 gem
- 在您的 app 图像中,在
docker-compose.yml
中您通过volumes_from
. 指定了 - 当您的应用程序容器启动时,它会执行
bundle check || bundle install
,一切顺利。
BUNDLE_PATH
的挂载点
这是一种可能的解决方案,但对我来说,它感觉有点违背 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 化的全部意义在于拥有一个隔离的、可重现的环境。
# .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
放入主服务的启动中,相反,更新由不同的服务管理。无论哪种方式,都不要在生产中使用这种方法。 运行 在构建步骤中没有安装其依赖项的服务至少会导致不必要的停机时间。