Migration guide

Upgrading from Rails 7.2 to 8.0, step by step

The checklist we actually use: Ruby first, deprecations, app:update, framework defaults, the removals that bite, and how to ship it in small PRs.

ANAbhay Nikam, Founder, Navio Labs

A Rails upgrade is rarely hard. It’s just easy to do badly: one giant branch, three weeks of merge conflicts, and a Friday deploy nobody wants to own.

Here’s how we take an app from Rails 7.2 to 8.0 without the drama. It’s the same process every time, and most of it is boring on purpose.

Before you touch the Gemfile

1. Make sure your tests can catch a break

The official upgrade guide opens with this, and it’s right. If you don’t have decent test coverage, you’ll be checking every changed behavior by hand. You don’t need 100%, but you do need tests around sign-in, payments, background jobs and anything else that would wake you up at night.

If the suite is thin, spend a few days writing request specs or system tests for the critical paths first. It’s the cheapest insurance you’ll ever buy.

2. Get onto a supported Ruby — separately

Rails 8.0 requires Ruby 3.2.0 or newer. Rails 7.2 needed 3.1, so plenty of apps are still on 3.1.

Upgrade Ruby in its own PR, on Rails 7.2, and deploy it. If something breaks, you’ll know it was Ruby and not Rails. We usually go straight to the newest Ruby the app’s gems support.

3. Get to the latest 7.2 patch and clear deprecations

Move to the newest 7.2.x release, then run your test suite and read the deprecation warnings. Rails tells you what’s about to be removed; 8.0 is when it actually happens. Fix them now, while the old behavior still works.

A trick that helps: make deprecations raise in test so nobody can add new ones.

# config/environments/test.rb
config.active_support.deprecation = :raise

Ship each batch of fixes as small PRs onto your main branch. Every one of them is safe on 7.2.

4. Check your gems

Run bundle outdated and look for anything that pins rails, activerecord or railties below 8. The usual suspects are admin panels, auth gems, and anything that monkey-patches Active Record. Upgrade those on 7.2 first where you can.

The upgrade itself

5. Bump Rails

# Gemfile
gem "rails", "~> 8.0.0"
bundle update rails

If Bundler can’t resolve, it will tell you which gem is holding things back. Fix that gem, not the lockfile.

If you use jsbundling-rails, update the Rails JavaScript packages in package.json too, then run bin/rails javascript:install.

6. Run bin/rails app:update

bin/rails app:update

This walks through Rails’ own files (configs, binstubs, initializers) and asks what to do with each difference. Read every diff. The safe default is to accept new files and carefully merge changed ones, keeping your customizations.

It also creates config/initializers/new_framework_defaults_8_0.rb. Leave config.load_defaults in config/application.rb at 7.2 for now. Your app keeps its old behavior until you opt in.

7. Fix what the removals broke

These are the 8.0 removals and deprecations we see most often in real apps. Search your codebase for each:

Change in 8.0What to do
config.read_encrypted_secrets removedDelete it; use credentials.
Extending the console via Rails::ConsoleMethods removedMove helpers into an initializer or a console-specific module.
config.active_record.commit_transaction_on_non_local_return removedDelete the setting; fix any return inside a transaction block.
config.active_record.warn_on_records_fetched_greater_than removedDelete it; use your APM or a query log instead.
Keyword-argument syntax for enum removedChange enum status: [...] to enum :status, [...].
config.action_controller.allow_deprecated_parameters_hash_equality removedDelete it; compare params.to_h explicitly if you relied on it.
form_with model: nil removedDrop the model: argument when there’s no model.
ActiveSupport::ProxyObject removedInherit from BasicObject instead.
config.active_job.enqueue_after_transaction_commit deprecatedRemove the app-wide setting; set self.enqueue_after_transaction_commit on the job classes that need it.
Benchmark.ms deprecatedUse Process.clock_gettime or your own helper.
bin/rake stats deprecatedUse bin/rails stats.

Two behavior changes are worth knowing about even if nothing errors:

  • Regexp.timeout now defaults to 1 second. A regex that runs longer raises instead of hanging the process. Good for security, surprising if you parse huge strings with complex patterns.
  • db:migrate on a fresh database loads the schema first instead of replaying every migration. If you relied on old migrations running from scratch (for example, migrations that seed data), use db:migrate:reset in that situation — and remember it drops and recreates the database.

8. Make the test suite green

Run everything. Fix failures one at a time. Resist the urge to also refactor; that’s a different PR.

9. Deploy on old defaults

Ship Rails 8.0 with load_defaults 7.2. This is the step people skip, and it’s the one that keeps upgrades boring. You’re only changing the framework version, not its behavior, so if something goes wrong in production it’s a much smaller haystack.

After the upgrade: flip the defaults

10. Adopt new defaults one at a time

Open config/initializers/new_framework_defaults_8_0.rb. Each setting is commented out. Uncomment one (or a related group), run tests, deploy, watch for a day. Repeat.

When every setting is enabled and you’re happy, delete the file and change:

# config/application.rb
config.load_defaults 8.0

11. Pick up the new toys — later

Solid Queue, Solid Cache, Kamal, Propshaft, the authentication generator, params.expect: none of them are required by the upgrade. Adopt them as separate projects, with their own PRs. Mixing “upgrade Rails” with “change our job backend” is how upgrades turn into incidents.

The one we do adopt quickly is params.expect, because it’s a small, safer replacement for require(...).permit(...):

# Before
params.require(:post).permit(:title, :body)

# After
params.expect(post: [:title, :body])

The checklist

Copy this into your PR description:

[ ] Critical paths covered by tests
[ ] Ruby >= 3.2 deployed on Rails 7.2 (separate PR)
[ ] Latest 7.2.x deployed, deprecation warnings cleared
[ ] Gems that pin Rails < 8 upgraded or replaced
[ ] Gemfile: rails ~> 8.0.0, bundle update rails
[ ] bin/rails app:update — every diff reviewed
[ ] Removed settings deleted, enum syntax updated
[ ] Test suite green
[ ] Deployed with load_defaults 7.2
[ ] new_framework_defaults_8_0.rb flipped one setting at a time
[ ] load_defaults 8.0, defaults file deleted

How long does it take?

For a healthy app with good tests, the Rails bump itself is often a day or two of work. The real time goes into the steps around it: getting Ruby current, clearing years of deprecations, and replacing an abandoned gem or two. That’s why we do those first, on the old version, where every change is safe to ship on its own.

Going on to 8.1 afterwards? It’s a much smaller step — we covered it in Rails 8.1: what changed.

If you’d rather have someone do this upgrade with you, that’s what we do.

Sources

More blogs

Rather have someone do this with you?

That’s what we do. Tell us where you’re stuck and you’ll hear back from the founder within one business day.

Start a project with Navio Labs
Get help with this