How to Generate a Gemfile.next.lock for Faster Rails Upgrades
“In anything at all, perfection is finally attained not when there is no longer anything to add, but when there is no longer anything to take away.” — Antoine de Saint-Exupéry
While Saint-Exupéry was writing about aviation design, this quote applies equally well to software development: the best solutions are often the ones that remove friction rather than add complexity. We often think of a software upgrade as an event — a singular moment in time when a system transitions from one state to another. This mental model, though, breaks down when applied to large Ruby on Rails applications. A “big bang” upgrade, where feature development is frozen while a team resolves hundreds of dependency conflicts and deprecation warnings, is risky. It typically creates stale upgrade branches and severe merge conflicts.
Before we get into the mechanics of dual booting, let’s take a step back and talk about how we manage dependencies. When we run bundle install, Bundler resolves our dependencies and writes the exact versions to a Gemfile.lock. This lockfile ensures that every developer — and our production environment — runs the exact same code.
However, during an upgrade, we need to transition to a new set of dependencies. We could, of course, maintain a completely separate file, perhaps named Gemfile.next, and use the BUNDLE_GEMFILE environment variable. This approach, though, requires keeping two separate lists of dependencies in sync by hand, which is error-prone.
A more robust solution is to use dual booting. By maintaining a secondary lockfile — the Gemfile.next.lock — our application can run against both its current Rails version and the target upgrade version simultaneously. This allows us to incrementally fix test failures without disrupting ongoing product development.
To manage this, we will use a Bundler plugin called bootboot, originally developed by Shopify.
Installing the Bootboot Plugin
Before we can generate our secondary lockfile, we need to install the bootboot plugin. Bundler plugins, strictly speaking, modify the behavior of Bundler itself rather than adding code to our application’s runtime.
Let’s install the plugin:
$ bundle plugin install bootboot
Fetching gem metadata from https://rubygems.org/.
Fetching bootboot 0.2.2
Installing bootboot 0.2.2
Installed plugin bootboot
This installs the plugin globally for your current Bundler environment.
Generating the Initial Gemfile.next.lock
Once the plugin is installed, we need to initialize it within our Rails project. Since bootboot relies on the existing state of your dependencies, it’s wise to ensure the latest “known good” version of your Gemfile and Gemfile.lock are committed to source control before you use any of the commands in this guide.
Navigate to your project root and run:
$ bundle bootboot
This command performs two critical actions. If we were to run git status right now, we would notice a few things:
$ git status
...
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: Gemfile
Untracked files:
(use "git add <file>..." to include in what will be committed)
Gemfile.next.lock
...
First, it appends plugin 'bootboot', '~> x.x' to the end of our Gemfile. Second, it copies our existing Gemfile.lock to create a new file named Gemfile.next.lock.
At this stage, our Gemfile.next.lock is an exact replica of our primary lockfile. To make it useful for an upgrade, we must configure our Gemfile to conditionally load the newer Rails version.
Configuring the Gemfile for Dual Dependencies
Let’s open our Gemfile and modify the Rails dependency declaration. We will use the DEPENDENCIES_NEXT environment variable — which bootboot looks for by default — to branch our dependencies.
Our configuration should resemble the following:
plugin 'bootboot', '~> 0.2'
if ENV['DEPENDENCIES_NEXT']
enable_dual_booting
gem 'rails', '~> 8.0.0'
else
gem 'rails', '~> 7.2.0'
end
One may wonder: if we have two lockfiles, why do we need the enable_dual_booting method call? The answer is straightforward. This method tells the plugin to sync changes between our primary and secondary lockfiles when appropriate. This ensures that when another developer updates an unrelated gem — perhaps a background worker gem — that update is reflected in both lockfiles, keeping them in sync.
Updating the Secondary Lockfile
With the conditional logic in place, we can now update the Gemfile.next.lock to resolve the dependencies for our target Rails version.
To instruct Bundler to use the secondary lockfile, we prefix our commands with the DEPENDENCIES_NEXT environment variable:
$ DEPENDENCIES_NEXT=1 bundle install
When we run this command, Bundler reads the Gemfile, sees the DEPENDENCIES_NEXT flag, and attempts to resolve dependencies for Rails 8.0. It will write the updated resolution exclusively to Gemfile.next.lock, leaving our production Gemfile.lock completely untouched.
Resolving Dependency Conflicts
Of course, in real-world applications, running the command above rarely succeeds on the first try. You will likely encounter version conflicts with third-party gems that are incompatible with the new Rails version.
Bundler might output an error message detailing the conflicting dependencies. For example, you might see output similar to this:
$ DEPENDENCIES_NEXT=1 bundle install
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
Bundler could not find compatible versions for gem "railties":
In Gemfile:
rails (~> 8.0.0) was resolved to 8.0.0, which depends on
railties (= 8.0.0)
devise was resolved to 4.9.2, which depends on
railties (< 8.0, >= 4.1.0)
In this case, an older version of the devise gem explicitly requires railties < 8.0.
To resolve these conflicts, we must update the offending gems specifically within the context of the next lockfile:
$ DEPENDENCIES_NEXT=1 bundle update devise
We repeat this process — updating individual gems or groups of gems — until DEPENDENCIES_NEXT=1 bundle install completes successfully. This iterative resolution is the core advantage of the Gemfile.next.lock workflow; we are solving the dependency puzzle in isolation, without breaking the application for other developers.
Maintaining the Gemfile.next.lock in CI
Generating the Gemfile.next.lock is only the first step. To actually execute a continuous upgrade, the secondary lockfile must be actively maintained and tested.
We should commit both Gemfile and Gemfile.next.lock to our version control system. Then, we can configure our Continuous Integration (CI) pipeline to run our test suite twice: once against the primary lockfile and once against the secondary lockfile.
For example, in GitHub Actions, a matrix build might look like this:
test:
runs-on: ubuntu-latest
strategy:
matrix:
gemfile_next: [0, 1]
env:
DEPENDENCIES_NEXT: ${{ matrix.gemfile_next }}
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- run: bundle exec rspec
continue-on-error: ${{ matrix.gemfile_next == 1 }}
Initially, we configure the test step with continue-on-error: ${{ matrix.gemfile_next == 1 }} to explicitly allow failures on the DEPENDENCIES_NEXT=1 build. This ensures that test failures related to the new Rails version do not block the merging of daily feature work.
Finalizing the Upgrade
As our team incrementally fixes deprecations and test failures, the Gemfile.next.lock build will eventually pass consistently.
At that point, the application is fully compatible with the new Rails version. The final migration to the new version is a matter of cleanup: we remove the conditional logic from our Gemfile, permanently require the new Rails version, and delete the Gemfile.next.lock and bootboot plugin.
By isolating the dependency resolution process into a Gemfile.next.lock, we transform a monolithic upgrade event into a manageable, continuous process.
Sponsored by Durable Programming
Need help maintaining or upgrading your Ruby on Rails application? Durable Programming specializes in keeping Rails apps secure, performant, and up-to-date.
Hire Durable Programming