UPDATE: We've come up with a potentially better way to achieve the same goal, by using a new Gemfile.initial file. See the new post here.

I wrote a small script that can sync <GEM>_VERSION environment variables in a Dockerfile with versions from Gemfile.lock.

For example, if you have the following files:

  • Dockerfile
ENV RAKE_VERSION="12.0.0" \
    RAILS_VERSION="5.0.0" \
    NOKOGIRI_VERSION="1.0.0"
  • Gemfile.lock
    rake (13.0.6)
...
    rails (6.0.5)
...
    nokogiri (1.13.6)

You can run this script to parse the versions from your Gemfile.lock and update your Dockerfile with the current versions:

#!/bin/bash
set -euo pipefail
ROOT_DIR="$(realpath $(dirname "$0")/..)"

(
  cd $ROOT_DIR
  # Set Dockerfile gem versions from Gemfile.lock
  for GEM_NAME in rake rails nokogiri; do
    GEM_VERSION="$(grep " $GEM_NAME (\d*\.\d*\.\d*)" Gemfile.lock | grep -o '\d*\.\d*\.\d*')"
    GEM_NAME_UPCASE=$(echo $GEM_NAME | tr '[:lower:]' '[:upper:]')
    sed 's/'$GEM_NAME_UPCASE'_VERSION="[^"]*"/'$GEM_NAME_UPCASE'_VERSION="'$GEM_VERSION'"/g' Dockerfile > Dockerfile.tmp
    mv Dockerfile.tmp Dockerfile
  done
)
I like to use a ROOT_DIR variable in my scripts so that I can call them from any directory.

You can also run this script automatically whenever you call bundle install. To do this, add a Gem.post_install hook to your Gemfile :

Gem.post_install do
  next if @updated_dockerfile
  system('scripts/update_dockerfile_versions.sh')
  @updated_dockerfile = true
end

Why would you need to do this?

It can take a long time to install gem dependencies when you're building a Docker image. To speed this up, you can put these lines near the top of your Dockerfile:

COPY Gemfile Gemfile.lock ./
RUN bundle install

Docker will cache the bundle install step, and it will only re-run this step if there are any changes in Gemfile or Gemfile.lock.

You can speed this up even further if you install a couple of specific gems even earlier in your Dockerfile. This can be especially helpful if there are some gems that take a long time to compile native extensions.

The following Dockerfile will always cache the current versions of rails, rake, and nokogiri, even if you've added or updated some other gems.

ENV RAKE_VERSION="13.0.6" \
    RAILS_VERSION="6.0.5" \
    NOKOGIRI_VERSION="1.13.6"

RUN echo 'gem: --no-document' >> ~/.gemrc && \
  gem install nokogiri -- --use-system-libraries --version "$NOKOGIRI_VERSION" && \
  gem install rake --version "$RAKE_VERSION" && \
  gem install rails --version "$RAILS_VERSION"

COPY Gemfile Gemfile.lock ./
RUN bundle install

-

Can you parse the gem versions while building the Dockerfile?

You would need to copy the Gemfile.lock into your Docker image in order to parse the versions. This would break the extra layer of caching we're trying to achieve, because Gemfile.lock is updated whenever any gem is changed.

What about using an .env file or -e flags?

You could also pass these environment variables as flags. You could write a script that wraps the docker-compose and docker commands and sets these environment variables. I think it might just be easier to update the versions directly in your Dockerfile, especially if you can automate it with a Gem.post_install hook.

Let us know if you have any suggestions or a better way to do this!

We don't have a commenting feature on our blog, but please feel free to send us an email: [email protected]