All posts

2 min read Nathan Broadbent

Automatically update Docker gem versions from Gemfile.lock

Pinned dependency rows flow from Gemfile.lock through a synchronization gear into matching Dockerfile rows.
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:

#!/usr/bin/env ruby
require 'bundler'

project_dir = File.expand_path('..', __dir__)
Dir.chdir(project_dir) do
  lockfile = Bundler::LockfileParser.new(Bundler.read_file('Gemfile.lock'))
  dockerfile = File.read('Dockerfile')

  %w[rake rails nokogiri].each do |name|
    versions = lockfile.specs.select { |spec| spec.name == name }
      .map { |spec| spec.version.to_s }.uniq
    raise "Expected one locked version for #{name}: #{versions.inspect}" unless versions.one?

    variable = "#{name.upcase}_VERSION"
    pattern = /\b#{Regexp.escape(variable)}="[^"]*"/
    raise "Expected one #{variable} assignment in Dockerfile" unless dockerfile.scan(pattern).one?

    dockerfile.sub!(pattern) { "#{variable}=\"#{versions.first}\"" }
  end

  # Write only after every version and assignment has been checked.
  File.write('Dockerfile', dockerfile)
end
Save this as scripts/update_dockerfile_versions.rb. It locates the project relative to the script, so it also works when the directory name contains spaces. Bundler parses the lockfile, including versions with four components, prereleases, and platform variants.

Run the script after Bundler finishes, so it reads the new lockfile. A Gem.post_install hook runs during individual gem installation and can read the old lockfile before Bundler writes the new one.

bundle install && ruby scripts/update_dockerfile_versions.rb

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 can reuse the bundle install layer while Gemfile, Gemfile.lock, and the preceding build layers are unchanged.

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 --version "$NOKOGIRI_VERSION" -- --use-system-libraries && \
  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?

Another option is to use Docker build arguments: declare ARG values in the Dockerfile and pass them with docker build --build-arg. Runtime -e flags do not configure a build. I chose to keep the versions in the Dockerfile and update them after bundle install.

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]

More posts

  1. A cut ruby gemstone fitted exactly inside a navy blueprint outline, connected to four code cards with teal check marks, above a tag reading typed: strict.

    14 min read

    Going all-in on Sorbet

    Sorbet sat in our Rails app for 18 months with a tenth of the code typed. Then AI agents typed the rest in four days, and we kept going.

  2. An SDK card and an API card, each connected by a line to a filled PDF form

    4 min read

    PDF form filler SDK vs API

    When a PDF library is enough to fill your forms, and when a hosted API saves you the work.

  3. A JSON data card connected by a line to a PDF form with filled fields, a ticked checkbox and a signature

    5 min read

    Best PDF form filling APIs (2026)

    DocSpring, Anvil, PDF.co, Adobe PDF Services, and Nutrient compared for filling fillable PDFs and flat PDFs.

Try DocSpring with your own documents

Upload a PDF or a Word document, or create an HTML template, then send a test request. No credit card needed.

Blog posts and product news by email

New articles and product changes, once a month.