Skip to main content

Ruby on Rails Major Version Upgrade Blueprint

Upgrade a Rails app one major version at a time using the dual-boot technique so both versions run from a single branch. Gems and Ruby are aligned first, deprecations are cleared, and new defaults are adopted incrementally under test.

From
Rails Legacy
To
Rails Current
Difficulty
Intermediate
Duration
16 weeks
Team Size
small

What and Why

Ruby on Rails major upgrades, for example 6 to 7 to 8, bring security fixes, performance gains, and modern defaults such as Hotwire for front-end interactivity and changes to the asset pipeline. Falling behind compounds risk because each skipped major version widens the gap and the eventual jump becomes far more dangerous. This blueprint upgrades one major version at a time using Rails' dual-boot technique, which lets a single branch run under both the current and target versions so regressions surface incrementally.

Phases

Assessment. Read the official Rails upgrade guide for the target version end to end; it lists breaking changes precisely. Inventory every gem in the Gemfile and check each against the new Rails and Ruby versions. Measure test coverage, because the suite is the safety net and gaps in it are where upgrade regressions slip through unnoticed.

Dependency upgrade. Upgrade Ruby first to a version the target Rails supports, then upgrade gems to releases compatible with the new Rails. Resolving the dependency graph before touching Rails itself avoids the common trap of fighting framework and library incompatibilities simultaneously.

Dual boot. Use the next_rails dual-boot setup so the application can load under both the current and target Rails versions from one codebase. Run the full test suite against both in CI to catch regressions one at a time rather than in a single overwhelming batch at the end.

Deprecation cleanup. Turn on deprecation logging and work through every warning. Adopt the new framework defaults gradually via config.load_defaults and the bin/rails app:update task, and migrate removed APIs such as update_attributes to update. Doing this incrementally keeps each change small and reviewable.

Cutover. Flip the default to the new version, remove the dual-boot scaffolding, and deploy. Wrap the highest-risk behavioral changes in feature flags so they can be toggled off in production without a redeploy if something misbehaves.

Key Risks and Mitigations

  • Dependency gaps: A critical gem may lag the new Rails release. During assessment, find a maintained fork or a replacement library before committing to the upgrade timeline.
  • Behavioral changes: New framework defaults change behavior in subtle ways. Adopt them one flag at a time with the test suite green between each, so a regression is attributable to a single change.
  • Downtime: Deploy incrementally and keep the previous release one git revert away, so recovery is fast if a regression reaches production.

Recommended Tooling

The next_rails gem for dual-boot, RSpec or Minitest for the test suite, Bundler for dependency resolution, and a CI matrix that runs both the current and target Rails versions until cutover.

Success Metrics

Track test coverage maintained through the upgrade, the lead time of the upgrade itself, and defect rate after cutover to confirm no behavioral regressions shipped to users.

Prerequisites

A solid automated test suite, a green CI pipeline before starting, and a compatible Ruby version available in both the build and runtime environments.

Sequencing and Rollback

The dual-boot setup is itself the rollback mechanism: until cutover, the default boot stays on the current Rails version and the target version is opt-in, so reverting is changing one environment variable. Sequence the work by clearing all deprecation warnings under the target version while still defaulting to the current one, then flip the default only when CI is green on both. Wrap behaviorally risky new defaults in feature flags so they can be disabled in production independently of the framework version itself. Confirm a representative staging environment runs the target Rails version against production-like data, since subtle query and serialization differences between major versions are best caught there rather than in production after the default has flipped.