The go-to resource for upgrading Ruby, Rails, and your dependencies.

Dual Booting Rails 7.2 and 8.0


On May 31st and June 1st of 1886, over a single weekend, the railroads of the southern United States changed their track gauge from five feet wide to the standard four feet, eight and a half inches. It was a massive, coordinated effort that required halting traffic and mobilizing tens of thousands of workers. It worked, but it was incredibly risky, disruptive, and required a massive halt to daily operations.

When upgrading a large Ruby on Rails application, many teams attempt a similar “big bang” approach. Upgrading a Ruby on Rails application, of course, is rarely a straightforward version bump. When you attempt to fix all deprecations, gem incompatibilities, and test failures in one monolithic pull request, you increase the risk of introducing severe bugs. Furthermore, the organization is forced to freeze features, halting product development entirely.

Dual booting offers a pragmatic alternative. By configuring your application to run on both Rails 7.2 and Rails 8.0 simultaneously, you enable a gradual, battle-tested workflow. Your engineering team can fix tests, resolve deprecations, and ensure technical health over time, rather than halting all other work.

What is Dual Booting?

Dual booting, strictly speaking, means maintaining two distinct sets of dependencies for a single application. In our case, one set uses Rails 7.2, and the other uses Rails 8.0. This strategy allows the main branch of your codebase to remain fully functional on the older version, while simultaneously passing Continuous Integration (CI) builds on the new version.

This approach is highly effective for technical debt remediation. It prevents the Rails upgrade from becoming a siloed project that inevitably falls behind the main development branch. Instead, the migration to the new version happens incrementally alongside daily feature work.

How to Set Up a Dual Boot Environment

There are a number of different approaches to implementing dual booting in a Ruby application.

The first option is the bootboot plugin for Bundler. This approach hooks directly into Bundler to generate a secondary lockfile (Gemfile.next.lock) while keeping your primary Gemfile.lock untouched.

The second option is the next_rails gem, which provides a suite of commands and scripts for managing dual dependencies.

Generally speaking, the bootboot plugin is the preferred approach because of its native integration as a Bundler plugin. This means it relies less on wrapper scripts and instead changes how Bundler resolves dependencies. Therefore, that is the approach we will be discussing here.

Step 1: Configure Bootboot

First, we must install the plugin and initialize it in our project.

Installation and Initialization

The first command installs the bootboot plugin for the current application. The second command, bundle bootboot, initializes the plugin. It modifies your Gemfile to enable dual booting and generates a Gemfile.next.lock to track the dependencies for the next version of Rails.

$ bundle plugin install bootboot
Installing plugin bootboot...
Installed plugin bootboot
$ bundle bootboot
Updating Gemfile...
File Gemfile updated.
Creating Gemfile.next.lock...
File Gemfile.next.lock created.

You may notice that running bundle bootboot generates a file that looks remarkably similar to your Gemfile.lock. This, of course, raises the question: how does Bundler know which file to use?

The answer is straightforward. We configure our Gemfile to conditionally load different Rails versions based on an environment variable.

Let’s modify our Gemfile to look something like this:

plugin 'bootboot', '~> 0.2'

if ENV['DEPENDENCIES_NEXT']
  enable_dual_booting
  gem 'rails', '~> 8.0.0'
else
  gem 'rails', '~> 7.2.0'
end

When you prefix a command with DEPENDENCIES_NEXT=1, Bundler resolves dependencies using the Gemfile.next.lock and loads Rails 8.0. Without the environment variable, it defaults to Rails 7.2.

Step 2: Update the Test Suite and CI Pipeline

Before we get into writing compatible code, though, we need to update our CI pipeline. We recommend running a matrix build that executes your entire test suite against both Rails versions.

Running Tests Locally

To test the next version of Rails on your local machine, run:

$ DEPENDENCIES_NEXT=1 bundle install
$ DEPENDENCIES_NEXT=1 bundle exec rspec

CI Matrix Configuration

In your CI configuration, you will want to set up a matrix strategy. One job should run the standard tests, and another should run with the DEPENDENCIES_NEXT=1 environment variable.

Initially, the Rails 8.0 build will likely fail. You should configure the CI pipeline to allow failures on the “next” build so it does not block merging daily feature work. Over time, your team can address these failures incrementally.

Step 3: Resolving Deprecations and Incompatibilities

With the CI pipeline exposing failures on the next Rails version, we can begin technical debt remediation.

When you encounter an API change between Rails 7.2 and Rails 8.0, you must write code that is compatible with both versions. We often use conditional logic to handle these discrepancies until the final cutover is complete.

For example, if you had a feature that relied on an API that behaves differently in Rails 8.0, you could use Gem::Version to check the current Rails version:

if Rails.gem_version >= Gem::Version.new("8.0.0")
  # Rails 8.0 implementation
else
  # Rails 7.2 implementation
end

While conditional logic adds temporary complexity to your application, it is a necessary trade-off for a safe upgrade process. Once the upgrade is finalized, you can safely remove the older implementation blocks.

Step 4: The Final Cutover

When the DEPENDENCIES_NEXT build passes consistently and all deprecation warnings are addressed, you are ready for the final cutover.

Because the application has been running and testing against Rails 8.0 in CI for weeks or months, the actual deployment is no longer a high-risk event. The cutover involves updating the primary Gemfile to require Rails 8.0, removing the bootboot plugin, and deleting the Gemfile.next.lock.

By treating a Ruby and Rails upgrade as a continuous process rather than a massive, isolated project, you drastically reduce security risks and ensure the long-term maintainability of your application. You also provide a smoother post-upgrade support experience, as the team has already become familiar with the new framework version during the dual-boot phase.

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