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 VersionEnd of Life
2.7March 2023
3.0March 2024
3.1March 2025
3.2March 2026
3.3March 2027

Gem audit

  • Run bundle outdated to list all outdated gems
  • Run bundler-audit check to 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 GemStatusReplacement
paperclipDeprecated (2018)active_storage (built into Rails 5.2+)
will_paginateUnmaintained for newer Railspagy or kaminari
attr_encryptedStalelockbox or Rails 7 encrypted attributes
cancanAbandonedcancancan (maintained fork)
therubyracerAbandonedmini_racer or remove if using Webpacker/jsbundling
coffee-railsDeprecatedRewrite CoffeeScript to ES6+
sass-railsReplaceddartsass-rails or cssbundling-rails
sprockets (< 4.0)Outdatedsprockets 4.x, propshaft, or jsbundling-rails
webpackerDeprecated (Rails 7)jsbundling-rails + cssbundling-rails
delayed_jobStalesolid_queue (Rails 8) or sidekiq
globalizeStalemobility

Test coverage assessment

  • Run your test suite. Record pass/fail ratio and total runtime
  • Install simplecov and 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 rubocop with default config and record offense count
  • Run rails_best_practices and review output
  • Run brakeman for security analysis
  • Check for monkey-patches on Rails internals (these break during upgrades)
  • Search for alias_method_chain usage (removed in Rails 5, replaced by Module#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:

  • ApplicationRecord base class introduced (replaces ActiveRecord::Base as parent)
  • ApplicationJob, ApplicationMailer base classes added
  • belongs_to requires optional: true for nullable associations
  • halt_callback_chain_on_return_false removed
  • rails command replaces rake for 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_attributes deprecated in favor of update
  • Host authorization middleware added (configure config.hosts)

Rails 6.x to 7.0:

  • Webpacker replaced by jsbundling-rails / importmap-rails
  • New encryption framework for Active Record
  • Async queries introduced
  • Rails.application.credentials changes
  • button_to generates <button> instead of <input type="submit">

Rails 7.0 to 7.1+:

  • Composite primary keys support
  • normalizes API for Active Record
  • Dockerfile generated by default
  • config.autoload_lib introduced

Risk assessment and rollback

  • Define rollback criteria: what failures trigger a rollback?
  • Ensure database migrations are reversible (write down methods)
  • 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 bundler itself first: gem install bundler (use latest stable)

Ruby version upgrade

Upgrade Ruby before Rails. Each Rails version has minimum Ruby requirements.

  • Update .ruby-version (or Gemfile ruby constraint) to target version
  • Run bundle install and 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: **kwargs separation enforced, frozen string literal behavior changes
    • Ruby 3.1: Psych 4.0 breaks YAML loading (use YAML.unsafe_load or permitted_classes)
    • Ruby 3.2: Struct keyword_init becomes opt-in, Object#=~ removed

Rails version upgrade (repeat per step)

For each minor/major version bump:

  1. Update rails gem version in Gemfile
  2. Run bundle update rails
  3. Run rails app:update and carefully review each generated diff
  4. Update config/application.rb to load new defaults: config.load_defaults X.Y
  5. Review and update config/initializers/new_framework_defaults_X_Y.rb
  6. Run rails db:migrate to verify migrations still work
  7. Run the full test suite
  8. Fix deprecation warnings (they become errors in the next major version)
  9. Deploy to staging and smoke test
  10. 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_file to has_one_attached
    • Run both systems in parallel before cutting over
  • 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/ to app/javascript/
    • Replace javascript_pack_tag with javascript_include_tag
    • Remove webpacker gem and config files
  • Sprockets to Propshaft (optional, at Rails 7.0+ step)

    • Replace sprockets-rails with propshaft in Gemfile
    • Move asset pipeline config from config/initializers/assets.rb
    • Ensure all asset paths use digested URLs
  • Coffee-rails removal

    • Convert .coffee files to .js or .es6
    • Use decaffeinate for automated conversion
    • Remove coffee-rails gem

Database considerations

  • Run rails db:migrate:status to 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 brakeman again and compare with Phase 1 baseline
  • Run bundler-audit check to 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-profiler on 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 brakeman with --confidence-level=1 for thorough scanning
  • Verify CSRF protection is active and functioning
  • Check that Content-Security-Policy headers are correct
  • Confirm SSL/TLS configuration after deploy
  • Review config/credentials.yml.enc and ensure secrets are not exposed
  • Verify that any new Rails security defaults are enabled (check new_framework_defaults files)

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

PhaseToolPurpose
Auditbundler-auditFind gems with known CVEs
AuditbrakemanStatic security analysis
AuditrubocopCode quality and style
Auditrails_best_practicesRails-specific code smells
AuditsimplecovTest coverage measurement
Planrails app:updateGenerate config diffs for new Rails version
Plannext_rails gemFind gems blocking your Rails upgrade
ExecutedecaffeinateConvert CoffeeScript to modern JS
Executedual_boot gemRun two Rails versions side-by-side
Validaterack-mini-profilerPerformance profiling
Validatederailed_benchmarksMemory 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.