Caching Gems with Docker Multi-Stage build

Installation of bundle takes a majority of the time in Dockerfile for a Rails build.

Here is the standard and shortened version of a Dockerfile for a Rails application.

FROM ruby:3.1.2

RUN gem install bundler:2.3.7


COPY Gemfile Gemfile.lock ./

RUN bundle install
COPY . .

The Dockerfile uses build cache which separates out Gemfile and Gemfile.lock before copying the source code.

This works great as Gems are only installed for the first time when the build is running.

Unlike non-dockerized environments, when there are changes made to the Gemfile, a gem is changed, or new ones added/removed, all the gems are re-installed from scratch.

Using Cache Image

RUN gem install bundler:2.3.7


# Copy the gems from a dedicated cache image
COPY --from saeloun:rails7:gem-cache /usr/local/bundle /usr/local/bundle
COPY Gemfile Gemfile.lock ./

RUN bundle install
COPY . .

It copies the /usr/local/bundle directory from the saeloun:rails7:gem-cache image to our build.

With COPY --from, Docker can copy files from an existing registry to the current build. By doing so, it is ensured that the Bundle does not start from scratch and has a cache of all the previously used gems.

The only problem with this that an image has to exist in the registry for this to work.

The issue with this approach is it will fail, if the initial build image does not exist.

MultiStage with Gem Caching

We can use MultiStage-build to refine the dockerfile further and pass the registry as docker build argument.

ARG BASE_IMAGE=ruby:3.1.2

# Build stage for the gem cache
FROM ${CACHE_IMAGE} AS gem-cache
RUN mkdir -p /usr/local/bundle

# Image with Bundler Installed
RUN gem install bundler:2.3.7
WORKDIR /usr/src/app

# Copy gems from a gem-cache build stage
FROM base AS gems
COPY --from=gem-cache /usr/local/bundle /usr/local/bundle
COPY Gemfile Gemfile.lock ./
RUN bundle install

# Get the source code in place
FROM base AS deploy
COPY --from=gems /usr/local/bundle /usr/local/bundle
COPY . .

The build stage has 4 Steps-

  1. gem-cache creates a directory for Gem Caching
  2. base installs bundler
  3. gem copies the gems from the existing image and runs bundler
  4. deploy add the source code

If CACHE_IMAGE is not set, the content of /usr/local/bundle directory will be empty and we won’t be able to copy any gems. If it is set to an image with gems, those will be copied over.

The image can now be built using

docker build .

After tagging, CACHE_IMAGE can be set as follows-

docker build .  --build-arg CACHE_IMAGE=saeloun:rails7:gem-cache app

Join Our Newsletter