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

Ruby on Rails Upgrade Guide

Learn how to keep your Rails applications modern and fast.

In 1913, the Ford Motor Company introduced the first moving assembly line for cars. Before this, assembling a Model T was a stationary event that took over twelve hours of effort. The assembly line changed the paradigm: instead of one massive, stationary effort, the work became a continuous, moving process.

Of course, we are not building cars — we are building software. Nevertheless, though, the same principle applies to upgrading a Ruby on Rails application. Treating an upgrade as a massive, once-every-five-years event often leads to frustration and systemic failure. Instead, upgrading Rails should be a continuous process.

The Continuous Upgrade Philosophy

Strictly speaking, the most effective way to manage upgrades is to stay close to the main branch of rails/rails, or at least stay up to date with point releases. When you do encounter a major version upgrade, having comprehensive test coverage is your greatest asset. We will explore a systematic approach to bridging the gap between major versions.

Preparing for the Upgrade

Before we change any code, we must understand our current state.

Resolving Deprecations

Always fix deprecation warnings on your current Rails version before attempting to jump to the next one. The Rails core team typically introduces deprecation warnings in minor versions and removes the underlying code in the subsequent major release.

Auditing Dependencies

We can audit our dependencies using Bundler:

$ bundle outdated
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...

Outdated gems included in the bundle:
  * devise (newest 4.9.3, installed 4.8.1, requested ~> 4.8)
  * nokogiri (newest 1.15.4, installed 1.13.10)
  * pg (newest 1.5.4, installed 1.4.6)

This command will show which non-Rails gems have newer versions available. Often, gem maintainers release new versions specifically to support upcoming Rails releases. You also may notice that Bundler shows the newly available versions compared to the currently installed and requested versions; this makes it straightforward to understand exactly which updates are minor and which are major.

Execution Strategies

There are two major approaches to working through a Rails upgrade; depending on the particular circumstances you find yourself in, one of them may be more appropriate than the other.

The first approach is a branch-based upgrade. You create a new git branch, update the dependencies, fix the tests, and eventually merge it back. This is most useful, in my experience, for smaller applications or applications with a very small engineering team.

The second approach is the dual-boot strategy. This involves configuring your application to run on the old Rails version and the new Rails version simultaneously. Tools like the next_rails gem facilitate this by allowing you to switch between your standard Gemfile.lock and a Gemfile.next.lock.

Generally speaking, the branch-based upgrade is less complex to set up. The dual-boot strategy, though, will often make more sense if you are upgrading a large application while a team continues to merge new features into the main branch.

Executing the Upgrade

Updating the Gemfile

When we are ready to proceed, we update our Gemfile to point to the new version. It is crucial to jump one minor version at a time — for example, from 6.0 to 6.1, and then from 6.1 to 7.0. Do not skip minor versions.

One may wonder: if we have comprehensive test coverage, why do we need to upgrade one minor version at a time?

The answer is straightforward. As mentioned earlier, deprecation warnings are typically introduced in minor versions and removed in the subsequent major version. If we skip minor versions, we lose the opportunity to see and address those deprecations before they become raised exceptions.

gem 'rails', '~> 7.0.0'

After updating the file, we can install the new version:

$ bundle update rails
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
Fetching rails 7.0.8
Installing rails 7.0.8
...snip...
Bundle updated!

I’ve abbreviated the above output for the sake of brevity, but the command will download and install the new Rails version and any updated dependencies.

Applying Framework Defaults

As a safety reminder, the rails app:update command modifies files in your repository; therefore, before you use this command, it is wise to ensure the latest known good version of your codebase is committed to source control.

Once Bundler has successfully resolved the new dependencies, we must update the framework’s configuration files. Rails provides an interactive task for this purpose:

$ rails app:update
       exist  config
    conflict  config/application.rb
Overwrite /Users/name/project/config/application.rb? (enter "h" for help) [Ynaqdhm] d

This task modifies your configuration files to match the new defaults. When prompted with a conflict, you can enter d to view the diff output. Carefully review all diff outputs. When in doubt, it is generally best to prefer the new Rails defaults and migrate your custom settings over them.

Testing and Remediation

Finally, we run our test suite against the new version and address any breaking changes. This is where the dual-boot strategy demonstrates its value — we can run tests against both our old lockfile and the new lockfile in our continuous integration environment:

$ bundle exec rspec
...snip...
0 failures

$ DEPENDENCIES_NEXT=1 bundle exec rspec
...snip...
0 failures

Running tests against both environments ensures we do not introduce regressions for our production environment while finalizing the upgrade.