Modernizing a legacy Rails application is not a weekend project. It demands a structured approach across four phases: audit, planning, execution, and validation. Skip a phase and you risk production outages, data loss, or a half-finished upgrade that stalls for months.
This checklist covers the concrete steps for upgrading Rails applications from older versions (3.x through 6.x) to Rails 7.x+, including Ruby version changes, gem replacement, and database considerations.
Engineering Insights from 15 Years of Rails Maintenance
After maintaining and modernizing Rails applications since 2009, we keep seeing the same patterns:
- Most legacy Rails apps we audit run Rails 4.x or 5.x on Ruby 2.5 or 2.6. These versions are end-of-life and no longer receive security patches. We wrote up what that means in practice in Ruby 2.6 EOL: the security vulnerabilities you inherit.
- Test coverage below 40% is the norm. Most legacy apps have either no tests or a fragile suite nobody trusts. This is the single biggest risk factor in any upgrade. Our guide on adding regression tests to legacy Rails apps covers how to build that safety net first.
- A typical modernization takes 3-6 months for a mid-sized app (50-150 models) with one dedicated developer. Apps without test coverage need an extra 4-8 weeks to write a baseline suite before the actual upgrade work can start.
- The most common blocker: orphaned gems. We regularly meet apps pinned to an old Rails version because a single gem has no maintained fork.
- Incremental upgrades beat big-bang rewrites. We upgrade one minor version at a time (5.0 to 5.1 to 5.2 to 6.0, and so on). Jumping multiple major versions in one step is the most common reason upgrade projects fail. Our legacy Rails modernization and Rails upgrade services follow this incremental approach.
If your system is older still, or you are weighing whether to leave Rails entirely, start with how to migrate legacy systems to modern web apps before committing to a path.
Phase 1: Audit the Existing Rails Stack
Before you change a single line of code, you need a complete picture of the current state.
Ruby and Rails version inventory
- Document the current Ruby version (
ruby -v) and Rails version (rails -v) - Check whether your Ruby version still receives security patches (Ruby Maintenance Branches)
- Check whether your Rails version is still supported (Rails Maintenance Policy)
- Define target versions for Ruby and Rails
Ruby end-of-life reference:
| Ruby version | End of life |
|---|---|
| 2.7 | March 2023 |
| 3.0 | March 2024 |
| 3.1 | March 2025 |
| 3.2 | March 2026 |
| 3.3 | March 2027 |
Gem audit
- Run
bundle outdatedto list all outdated gems - Run
bundler-audit checkto identify gems with known CVEs - Flag gems that are orphaned (no commits for 2+ years, no response to issues)
- Identify gems that do not work with the target Rails version
- Look for gems that pin specific Rails or Ruby versions in their gemspec
Commonly problematic gems in legacy apps:
| Legacy gem | Status | Replacement |
|---|---|---|
paperclip | Deprecated (2018) | active_storage (built into Rails 5.2+) |
will_paginate | Unmaintained for newer Rails | pagy or kaminari |
attr_encrypted | Outdated | lockbox or Rails 7 encrypted attributes |
cancan | Abandoned | cancancan (maintained fork) |
therubyracer | Abandoned | mini_racer, or remove with webpacker/jsbundling |
coffee-rails | Deprecated | Rewrite CoffeeScript in ES6+ |
sass-rails | Superseded | dartsass-rails or cssbundling-rails |
sprockets (< 4.0) | Outdated | sprockets 4.x, propshaft, or jsbundling-rails |
webpacker | Deprecated (Rails 7) | jsbundling-rails + cssbundling-rails |
delayed_job | Outdated | solid_queue (Rails 8) or sidekiq |
globalize | Outdated | mobility |
For the delayed_job row specifically, we documented a full production migration in migrating Delayed::Job to Sidekiq.
Assess test coverage
- Run the test suite. Document the pass/fail ratio and total runtime
- Install
simplecovand measure line coverage percentage (our SimpleCov setup guide has a working configuration) - Identify critical paths without coverage (authentication, payments, core business logic)
- Assess test quality: do the tests actually verify behavior, or do they just execute code without checking results?
Infrastructure and dependencies
- Document the database version (PostgreSQL, MySQL) and check compatibility with the target Rails version
- List all external service integrations (payment gateways, email providers, APIs)
- Check Redis/Memcached versions if used for caching or background jobs
- Document the deployment pipeline (Capistrano, Docker, Heroku, etc.)
- Record the current Ruby process manager (Puma, Unicorn, Passenger)
Capture a code quality baseline
- Run
rubocopwith the standard config and document the violation count - Run
rails_best_practicesand review the output - Run
brakemanfor static security analysis - Search for monkey patches on Rails internals (these break during upgrades)
- Search for
alias_method_chain(removed in Rails 5, replaced byModule#prepend)
Phase 2: Plan the Upgrade Path
Define the version ladder
Never skip major versions. Upgrade one minor version at a time within each major, then move to the next major.
Example upgrade path for a Rails 4.2 app targeting Rails 7.2:
Rails 4.2 / Ruby 2.3
-> Rails 5.0 / Ruby 2.4
-> Rails 5.1 / Ruby 2.5
-> Rails 5.2 / Ruby 2.6
-> Rails 6.0 / Ruby 2.7
-> Rails 6.1 / Ruby 3.0
-> Rails 7.0 / Ruby 3.1
-> Rails 7.1 / Ruby 3.2
-> Rails 7.2 / Ruby 3.3
Each step is a separate production deployment. Do not bundle multiple version jumps.
- Map your specific version path from current to target state
- For each step, read the Rails Upgrade Guide for that version
- Estimate effort per step (typical: 1-3 weeks per minor version jump)
- Identify the hardest step (usually the major boundaries: 4.x to 5.0, 5.x to 6.0, 6.x to 7.0)
Breaking changes by Rails version
Rails 4.x to 5.0:
ApplicationRecordbecomes the new base class (replacingActiveRecord::Baseas parent)ApplicationJobandApplicationMailerbase classes addedbelongs_torequiresoptional: truefor nullable associationshalt_callback_chain_on_return_falseremoved- The
railscommand replacesrakefor most tasks
Rails 5.x to 6.0:
- Autoloader switches from classic to Zeitwerk (autoload issues must be fixed)
- Action Cable, Active Storage, Action Mailbox, Action Text added as defaults
update_attributesdeprecated in favor ofupdate- Host authorization middleware added (configure
config.hosts)
Rails 6.x to 7.0:
webpackerreplaced byjsbundling-rails/importmap-rails- New encryption framework for Active Record
- Async queries introduced
- Changes to
Rails.application.credentials button_torenders<button>instead of<input type="submit">
Rails 7.0 to 7.1+:
- Composite primary key support
normalizesAPI for Active RecordDockerfilegenerated by defaultconfig.autoload_libintroduced
Risk assessment and rollback plan
- Define rollback criteria: which failures trigger a rollback?
- Make sure database migrations are reversible (write
downmethods) - Plan feature flags to isolate upgraded code paths
- Set up a staging environment mirroring production data (anonymized)
- Document the rollback procedure for every upgrade step
Allocate resources
- Assign one dedicated developer (or a pair) to the upgrade. Context switching kills upgrade projects
- Block upgrade time in sprint planning. Upgrades done “when we have time” never finish
- Schedule code-freeze periods for major version jumps
- Estimate the total budget (our rule of thumb: 2-4 developer weeks per major Rails version jump)
Phase 3: Execute the Upgrade
Preparation
- Create a long-lived feature branch for the upgrade (
upgrade/rails-X.Y) - Set up CI to run tests against the upgrade branch
- If test coverage is below 60%: write tests for critical paths before the upgrade starts
- Back up the production database
- Update
bundleritself first:gem install bundler(latest stable)
Upgrade the Ruby version
Upgrade Ruby before Rails. Every Rails version has a minimum Ruby requirement.
- Update
.ruby-version(or the Gemfile Ruby constraint) to the target version - Run
bundle installand resolve gem compatibility issues - Run the test suite. Fix failures caused by Ruby syntax/behavior changes
- Watch for these common Ruby upgrade issues:
- Ruby 2.7: keyword argument separation warnings (become errors in 3.0)
- Ruby 3.0:
**kwargsseparation enforced, frozen string literal changes - Ruby 3.1:
Psych 4.0breaks YAML loading (useYAML.unsafe_loadorpermitted_classes) - Ruby 3.2:
Structkeyword_init becomes opt-in,Object#=~removed
Upgrade the Rails version (repeat per step)
For every minor/major version jump:
- Update the
railsgem version in theGemfile - Run
bundle update rails - Run
rails app:updateand review every generated diff carefully - Update
config/application.rb:config.load_defaults X.Y - Review and apply
config/initializers/new_framework_defaults_X_Y.rb - Run
rails db:migrateto verify migrations - Run the full test suite
- Fix deprecation warnings (they become errors in the next major version)
- Deploy to staging and smoke-test
- Deploy to production
Gem replacement checklist
Perform these gem switches at the matching Rails version step:
-
Paperclip to Active Storage (at the Rails 5.2 step)
- Install Active Storage:
rails active_storage:install - Migrate file metadata into Active Storage tables
- Switch model attachments from
has_attached_filetohas_one_attached - Run both systems in parallel before cutting over
- Install Active Storage:
-
Webpacker to jsbundling-rails (at the Rails 7.0 step)
- Install:
rails new myapp -j esbuild(or add to an existing project) - Move JS entry points from
app/javascript/packs/toapp/javascript/ - Replace
javascript_pack_tagwithjavascript_include_tag - Remove the
webpackergem and its config files
- Install:
-
Sprockets to Propshaft (optional, Rails 7.0+)
- Replace
sprockets-railswithpropshaftin the Gemfile - Port asset pipeline config from
config/initializers/assets.rb - Make sure all asset paths use digested URLs
- Replace
-
Remove coffee-rails
- Convert
.coffeefiles to.jsor.es6 - Use decaffeinate for automated conversion
- Remove the
coffee-railsgem
- Convert
Database considerations
- Run
rails db:migrate:statusto check for pending or missing migrations - Verify all migrations run from scratch:
rails db:drop db:create db:migrate - If upgrading PostgreSQL at the same time: test with the new PG version on staging first
- Review Active Record changes: renamed methods, changed default scopes
- If you use
schema.rb: regenerate after each Rails version jump:rails db:schema:dump
Phase 4: Validation and Hardening
Regression testing
- Run the full test suite. Zero failures before the production deploy
- Re-run
brakemanand compare against the Phase 1 baseline - Run
bundler-audit checkto confirm no new vulnerabilities - Manual smoke tests on critical user flows (login, checkout, admin panels)
- Test background jobs: verify processing under the new Rails version
Performance benchmarking
- Compare response times for key endpoints (before vs. after)
- Check memory usage of the new Ruby/Rails version under load
- Run
rack-mini-profileron critical pages to catch N+1 queries or slow views - Verify caching (fragment cache, Russian doll caching, HTTP cache headers)
- Load-test staging with realistic traffic patterns
If the upgraded app feels slower, work through the 7 most common Ruby performance issues before blaming the new version.
Security validation
- Run
brakemanwith--confidence-level=1for thorough scanning - Verify CSRF protection
- Review
Content-Security-Policyheaders - Confirm SSL/TLS configuration after deploy
- Review
config/credentials.yml.encand make sure no secrets are exposed - Enable new Rails security defaults (check the
new_framework_defaultsfiles)
Post-upgrade cleanup
- Remove deprecated gem versions and unused gems from the
Gemfile - Delete old migration files if your team follows that practice (keep
schema.rb/structure.sql) - Update CI configuration: test only against the new Ruby/Rails versions
- Update documentation and README with new version requirements
- Archive the upgrade branch after merge
- Schedule the next upgrade (set a calendar reminder for 6 months out)
Quick Reference: Tools for Each Phase
| Phase | Tool | Purpose |
|---|---|---|
| Audit | bundler-audit | Find gems with known CVEs |
| Audit | brakeman | Static security analysis |
| Audit | rubocop | Code quality and style |
| Audit | rails_best_practices | Rails-specific code smells |
| Audit | simplecov | Measure test coverage |
| Planning | rails app:update | Generate config diffs for the new Rails version |
| Planning | next_rails gem | Find gems blocking the Rails upgrade |
| Execution | decaffeinate | Convert CoffeeScript to modern JS |
| Execution | dual_boot gem | Run two Rails versions side by side |
| Validation | rack-mini-profiler | Performance profiling |
| Validation | derailed_benchmarks | Memory and boot time analysis |
Frequently Asked Questions
How long does a full Rails upgrade take?
For a single major version jump (e.g. Rails 5.2 to 6.1), plan 4-8 weeks of focused work for a mid-sized app. Apps with low test coverage need extra time up front to build a safety net. Upgrades spanning multiple major versions (e.g. 4.2 to 7.2) typically stretch over 3-6 months.
Can I skip Rails versions when upgrading?
You can skip minor versions within the same major (e.g. going straight from 6.0 to 6.1). But never skip major versions. The internal API changes are too extensive, and you miss the critical deprecation warnings that guide the upgrade path.
What if a critical gem does not support the target Rails version?
Three options: (1) find a maintained fork on GitHub, (2) vendor the gem and patch it yourself, or (3) replace it with an alternative. The next_rails gem helps identify blocking gems before you start.
Should I upgrade Ruby or Rails first?
Upgrade Ruby first, to the minimum version your target Rails release requires, then upgrade Rails. This way you avoid debugging Ruby and Rails issues at the same time.
Is an upgrade worth it, or should we rewrite?
In the vast majority of cases: upgrade. In our experience a rewrite costs 3-5x more than an incremental upgrade and risks losing business logic that accumulated over years. Rewrites only make sense when the codebase is so rotten that no test passes and the original developers are gone.
Need Help With Your Rails Modernization?
Modernizing a legacy system is a high-stakes project. We turn that risk into a competitive advantage. If you are ready to bring your Rails application into the modern era, start with a code audit or talk to us.