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 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 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 gemStatusReplacement
paperclipDeprecated (2018)active_storage (built into Rails 5.2+)
will_paginateUnmaintained for newer Railspagy or kaminari
attr_encryptedOutdatedlockbox or Rails 7 encrypted attributes
cancanAbandonedcancancan (maintained fork)
therubyracerAbandonedmini_racer, or remove with webpacker/jsbundling
coffee-railsDeprecatedRewrite CoffeeScript in ES6+
sass-railsSupersededdartsass-rails or cssbundling-rails
sprockets (< 4.0)Outdatedsprockets 4.x, propshaft, or jsbundling-rails
webpackerDeprecated (Rails 7)jsbundling-rails + cssbundling-rails
delayed_jobOutdatedsolid_queue (Rails 8) or sidekiq
globalizeOutdatedmobility

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 simplecov and 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 rubocop with the standard config and document the violation count
  • Run rails_best_practices and review the output
  • Run brakeman for static security analysis
  • Search for monkey patches on Rails internals (these break during upgrades)
  • Search for alias_method_chain (removed in Rails 5, replaced by Module#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:

  • ApplicationRecord becomes the new base class (replacing ActiveRecord::Base as parent)
  • ApplicationJob and ApplicationMailer base classes added
  • belongs_to requires optional: true for nullable associations
  • halt_callback_chain_on_return_false removed
  • The rails command replaces rake for 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_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
  • Changes to Rails.application.credentials
  • button_to renders <button> instead of <input type="submit">

Rails 7.0 to 7.1+:

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

Risk assessment and rollback plan

  • Define rollback criteria: which failures trigger a rollback?
  • Make sure database migrations are reversible (write down methods)
  • 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 bundler itself 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 install and 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: **kwargs separation enforced, frozen string literal 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

Upgrade the Rails version (repeat per step)

For every minor/major version jump:

  1. Update the rails gem version in the Gemfile
  2. Run bundle update rails
  3. Run rails app:update and review every generated diff carefully
  4. Update config/application.rb: config.load_defaults X.Y
  5. Review and apply config/initializers/new_framework_defaults_X_Y.rb
  6. Run rails db:migrate to verify migrations
  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

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_file to has_one_attached
    • Run both systems in parallel before cutting over
  • 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/ to app/javascript/
    • Replace javascript_pack_tag with javascript_include_tag
    • Remove the webpacker gem and its config files
  • Sprockets to Propshaft (optional, Rails 7.0+)

    • Replace sprockets-rails with propshaft in the Gemfile
    • Port asset pipeline config from config/initializers/assets.rb
    • Make sure all asset paths use digested URLs
  • Remove coffee-rails

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

Database considerations

  • Run rails db:migrate:status to 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 brakeman and compare against the Phase 1 baseline
  • Run bundler-audit check to 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-profiler on 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 brakeman with --confidence-level=1 for thorough scanning
  • Verify CSRF protection
  • Review Content-Security-Policy headers
  • Confirm SSL/TLS configuration after deploy
  • Review config/credentials.yml.enc and make sure no secrets are exposed
  • Enable new Rails security defaults (check the new_framework_defaults files)

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

PhaseToolPurpose
Auditbundler-auditFind gems with known CVEs
AuditbrakemanStatic security analysis
AuditrubocopCode quality and style
Auditrails_best_practicesRails-specific code smells
AuditsimplecovMeasure test coverage
Planningrails app:updateGenerate config diffs for the new Rails version
Planningnext_rails gemFind gems blocking the Rails upgrade
ExecutiondecaffeinateConvert CoffeeScript to modern JS
Executiondual_boot gemRun two Rails versions side by side
Validationrack-mini-profilerPerformance profiling
Validationderailed_benchmarksMemory 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.