Modernizing a legacy Rails app is not a weekend project. It requires a structured approach across four phases: audit, plan, execute, and validate. Skip a phase and you risk production outages, data loss, or a half-finished upgrade that stalls for months.
This checklist covers the specific steps for upgrading Rails applications from older versions (3.x through 6.x) to Rails 7.x+, including Ruby version bumps, gem replacements, and database changes.
USEO’s Take
After 15+ years of maintaining and modernizing Rails applications, here is what we consistently see:
- 80% of legacy Rails apps we audit run Rails 4.x or 5.x with Ruby 2.5 or 2.6. These versions are past end-of-life and receive no security patches.
- Test coverage below 40% is the norm. Most legacy apps have either no tests or a brittle test suite that nobody trusts. This is the single biggest risk factor in any upgrade.
- The typical modernization timeline is 3-6 months for a mid-size app (50-150 models), assuming one dedicated developer. Apps with zero test coverage add 4-8 weeks for writing a baseline test suite before any upgrade work begins.
- The most common blockers are abandoned gems. We regularly encounter apps locked to old Rails versions because of a single gem with no maintained fork.
- Incremental upgrades beat big-bang rewrites. We upgrade one minor Rails version at a time (5.0 to 5.1 to 5.2 to 6.0, etc.). Jumping multiple major versions in one step is how projects fail.
Phase 1: Auditing Your Legacy Rails Stack
Before touching any code, you need a full picture of what you are working with.
Ruby and Rails version inventory
- Record current Ruby version (
ruby -v) and Rails version (rails -v) - Check if your Ruby version is still receiving security patches (Ruby maintenance branches)
- Check if your Rails version is still supported (Rails maintenance policy)
- Document the target Ruby and Rails versions
Ruby EOL 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 abandoned (no commits in 2+ years, no response to issues)
- Identify gems that will not work with your target Rails version
- Check 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 | Stale | lockbox or Rails 7 encrypted attributes |
cancan | Abandoned | cancancan (maintained fork) |
therubyracer | Abandoned | mini_racer or remove if using Webpacker/jsbundling |
coffee-rails | Deprecated | Rewrite CoffeeScript to ES6+ |
sass-rails | Replaced | 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 | Stale | solid_queue (Rails 8) or sidekiq |
globalize | Stale | mobility |
Test coverage assessment
- Run your test suite. Record pass/fail ratio and total runtime
- Install
simplecovand measure line coverage percentage - Identify critical paths with zero test coverage (authentication, payments, core business logic)
- Assess test quality: are tests actually asserting behavior, or just running code without checking results?
Infrastructure and dependencies
- Document database version (PostgreSQL, MySQL) and check compatibility with 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 current Ruby process manager (Puma, Unicorn, Passenger)
Code quality baseline
- Run
rubocopwith default config and record offense count - Run
rails_best_practicesand review output - Run
brakemanfor security analysis - Check for monkey-patches on Rails internals (these break during upgrades)
- Search for
alias_method_chainusage (removed in Rails 5, replaced byModule#prepend)
Phase 2: Planning the Upgrade Path
Define the version ladder
Never skip major Rails versions. Upgrade one minor version at a time within each major, then jump 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 deploy to production. Do not batch multiple version jumps.
- Map out your specific version ladder from current to target
- For each step, review the Rails upgrade guide for that version
- Estimate effort per step (typically 1-3 weeks per minor version bump)
- Identify the hardest step (usually the major version boundaries: 4.x to 5.0, 5.x to 6.0, 6.x to 7.0)
Breaking changes to plan for
Rails 4.x to 5.0:
ApplicationRecordbase class introduced (replacesActiveRecord::Baseas parent)ApplicationJob,ApplicationMailerbase classes addedbelongs_torequiresoptional: truefor nullable associationshalt_callback_chain_on_return_falseremovedrailscommand replacesrakefor most tasks
Rails 5.x to 6.0:
- Autoloader switches from classic to Zeitwerk (must fix any autoload issues)
- 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
Rails.application.credentialschangesbutton_togenerates<button>instead of<input type="submit">
Rails 7.0 to 7.1+:
- Composite primary keys support
normalizesAPI for Active RecordDockerfilegenerated by defaultconfig.autoload_libintroduced
Risk assessment and rollback
- Define rollback criteria: what failures trigger a rollback?
- Ensure database migrations are reversible (write
downmethods) - Plan for feature flags to isolate upgraded code paths
- Set up a staging environment that mirrors production data (anonymized)
- Document the rollback procedure for each upgrade step
Resource allocation
- Assign a dedicated developer (or pair) to the upgrade. Context switching kills upgrade projects
- Block time for the upgrade in sprint planning. Upgrades done “when we have time” never finish
- Plan for code freeze periods during major version jumps
- Estimate total budget (our rule of thumb: 2-4 developer-weeks per major Rails version jump)
Phase 3: Executing the Upgrade
Pre-upgrade prep
- 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 starting
- Back up the production database
- Update
bundleritself first:gem install bundler(use latest stable)
Ruby version upgrade
Upgrade Ruby before Rails. Each Rails version has minimum Ruby requirements.
- Update
.ruby-version(orGemfileruby constraint) to target version - Run
bundle installand fix any gem compatibility issues - Run the test suite. Fix any failures caused by Ruby syntax/behavior changes
- Watch for these common Ruby upgrade breakages:
- Ruby 2.7: keyword argument separation warnings (becomes errors in 3.0)
- Ruby 3.0:
**kwargsseparation enforced, frozen string literal behavior changes - Ruby 3.1:
Psych 4.0breaks YAML loading (useYAML.unsafe_loadorpermitted_classes) - Ruby 3.2:
Structkeyword_init becomes opt-in,Object#=~removed
Rails version upgrade (repeat per step)
For each minor/major version bump:
- Update
railsgem version inGemfile - Run
bundle update rails - Run
rails app:updateand carefully review each generated diff - Update
config/application.rbto load new defaults:config.load_defaults X.Y - Review and update
config/initializers/new_framework_defaults_X_Y.rb - Run
rails db:migrateto verify migrations still work - 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
Handle these gem swaps during the appropriate Rails version step:
-
Paperclip to Active Storage (at Rails 5.2 step)
- Install Active Storage:
rails active_storage:install - Migrate file metadata to Active Storage tables
- Update model attachments from
has_attached_filetohas_one_attached - Run both systems in parallel before cutting over
- Install Active Storage:
-
Webpacker to jsbundling-rails (at Rails 7.0 step)
- Install:
rails new myapp -j esbuild(or add to existing) - Move JS entry points from
app/javascript/packs/toapp/javascript/ - Replace
javascript_pack_tagwithjavascript_include_tag - Remove
webpackergem and config files
- Install:
-
Sprockets to Propshaft (optional, at Rails 7.0+ step)
- Replace
sprockets-railswithpropshaftin Gemfile - Move asset pipeline config from
config/initializers/assets.rb - Ensure all asset paths use digested URLs
- Replace
-
Coffee-rails removal
- Convert
.coffeefiles to.jsor.es6 - Use decaffeinate for automated conversion
- Remove
coffee-railsgem
- Convert
Database considerations
- Run
rails db:migrate:statusto check for pending or missing migrations - Test all migrations can run from scratch:
rails db:drop db:create db:migrate - If upgrading PostgreSQL alongside Rails, test with the new PG version in staging first
- Review Active Record changes: check for renamed methods, changed default scopes
- If using
schema.rb, regenerate it after each Rails version bump:rails db:schema:dump
Phase 4: Validation and Hardening
Regression testing
- Run the full test suite. Zero failures required before production deploy
- Run
brakemanagain and compare with Phase 1 baseline - Run
bundler-audit checkto confirm no new vulnerabilities - Execute manual smoke tests on critical user flows (login, checkout, admin panels)
- Test background jobs: verify they process correctly with the new Rails version
Performance benchmarking
- Compare response times for key endpoints (before vs. after upgrade)
- 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 that caching still works (fragment cache, Russian doll caching, HTTP cache headers)
- Load test the staging environment with realistic traffic patterns
Security validation
- Run
brakemanwith--confidence-level=1for thorough scanning - Verify CSRF protection is active and functioning
- Check that
Content-Security-Policyheaders are correct - Confirm SSL/TLS configuration after deploy
- Review
config/credentials.yml.encand ensure secrets are not exposed - Verify that any new Rails security defaults are enabled (check
new_framework_defaultsfiles)
Post-upgrade cleanup
- Remove deprecated gem versions and unused gems from
Gemfile - Delete old migration files if your team follows that practice (keep
schema.rb/structure.sql) - Update CI configuration to test against the new Ruby and Rails versions only
- 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)
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 | Test coverage measurement |
| Plan | rails app:update | Generate config diffs for new Rails version |
| Plan | next_rails gem | Find gems blocking your Rails upgrade |
| Execute | decaffeinate | Convert CoffeeScript to modern JS |
| Execute | dual_boot gem | Run two Rails versions side-by-side |
| Validate | rack-mini-profiler | Performance profiling |
| Validate | derailed_benchmarks | Memory and boot time analysis |
FAQs
How long does a full Rails upgrade take?
For a single major version jump (e.g., Rails 5.2 to 6.1), expect 4-8 weeks of focused work for a mid-size app. Apps with low test coverage need additional time upfront to build a safety net. Multi-major-version jumps (e.g., 4.2 to 7.2) typically span 3-6 months.
Can I skip Rails versions during an upgrade?
You can skip minor versions within the same major (e.g., go from 6.0 straight to 6.1). But never skip major versions. The internal API changes are too significant, and you will miss 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 which gems are blocking your upgrade before you start.
Should I upgrade Ruby or Rails first?
Upgrade Ruby first to the minimum version required by your target Rails version, then upgrade Rails. This avoids having to debug Ruby and Rails issues simultaneously.