Skip to content
ExponaBLOG
RUBY ON RAILS · DAY 066 / 100

Rails upgrades: Ruby on Rails best practices explained

Stay current with less pain.

Expona Admin6 min read

Why Rails upgrades matter

Rails upgrades aren't optional maintenance tasks—they're critical for security, performance, and team productivity. Staying current with Ruby on Rails best practices explained through a structured upgrade process keeps your application safe from vulnerabilities, gives you access to modern features, and prevents the compounding pain of skipping multiple versions at once.

We've seen teams delay upgrades only to face crisis mode when a security patch forces their hand or when onboarding new developers becomes difficult because the codebase is three major versions behind. The key to making Rails upgrades manageable is adopting a methodical approach: upgrade one minor version at a time, fix deprecation warnings first, run the built-in update command, and lean heavily on a strong test suite. This article walks through that exact process with real examples from production applications.

Upgrade one minor version at a time

The single most effective strategy we've used across multiple projects is to never jump more than one minor version. Jumping from Rails 6.0 to 6.1 is manageable. Jumping from Rails 6.0 to 7.0 is a major undertaking. Attempting to go from 5.2 directly to 7.0 is a recipe for a week-long debugging marathon.

Here's why incremental upgrades work: each minor version introduces breaking changes, API shifts, and dependency updates in smaller batches. When you attempt a multi-version jump, you lose the ability to pinpoint which change broke your code. You also miss the intermediate deprecation warnings that guide you toward solutions.

Start by identifying your current Rails version and planning a realistic upgrade path.

ruby
bundle exec rails --version
# Rails 6.0.6.1

In this case, your path looks like: 6.0.6.1 → 6.1.x → 7.0.x → 7.1.x. Each step should take between three days and two weeks depending on application size and complexity. Don't treat it as a side project. Allocate dedicated time on your sprint.

Update your Gemfile to target the next minor version:

ruby
# Gemfile
gem 'rails', '~> 6.1.0'

The tilde allows patch-level updates but locks the minor version. Run bundle update rails and commit separately from any code changes. This keeps your diff clean and reviewable.

Fix deprecation warnings first

Before touching your Gemfile, run your full test suite and production logs through a deprecation detection pass. Rails and its ecosystem are explicit about what's changing. Heeding these warnings early makes the actual version bump smoother.

Enable deprecation warnings in your test environment:

ruby
# config/environments/test.rb
config.active_support.deprecation = :stderr

Now run your full test suite and capture warnings to a file:

ruby
bundle exec rails test 2>&1 | grep -i deprecat > deprecations.log

Real example: if you're moving from Rails 6.0 to 6.1, you'll likely see warnings about rails/all boot, changes to how cookies are serialized, and updates to Active Storage. Each warning comes with explicit guidance.

A common deprecation in recent Rails versions concerns render :inline:

ruby
# Rails 6.0 - deprecated
render inline: '<p>Hello</p>'

# Rails 6.1+ - correct approach
render plain: 'Hello' # or render html: '<p>Hello</p>'

Address deprecations methodically. Create a checklist and tackle them in your current version before upgrading. This gives you two benefits: your codebase becomes cleaner, and when you run the upgrade command, you'll see fewer surprises. Also, if you're working in a team, you can tackle deprecations across parallel branches to speed up the process.

Use rails app:update

Rails provides a built-in command specifically designed to handle the mechanical parts of upgrading: rails app:update. This command compares your application's configuration files against the latest Rails template and offers to merge changes.

ruby
bundle exec rails app:update

When you run this, Rails presents an interactive prompt for each file that has changed. You'll see options like:

  • Y to accept the new version
  • N to keep your current version
  • D to see a diff
  • M to merge interactively

Let's walk through a realistic scenario. Moving from Rails 6.0 to 6.1, you might get prompted on config/boot.rb:

ruby
# Rails 6.0 style
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)

# Rails 6.1 introduces additional Bundler setup
require 'bundler/setup'

Most of the time, you want to accept these changes. They exist for good reason. However, if your application has custom configuration—say, a specialized boot process or non-standard gem paths—you might choose to keep your version and merge manually later.

The rails app:update command handles:

  • Configuration file updates (boot.rb, application.rb, environment files)
  • Database configuration changes
  • New default middleware configurations
  • Zeitwerk autoloading transitions (critical in Rails 6.0+)

Run this command immediately after bumping your Gemfile version and running bundle update. Commit the results separately so you can easily review what Rails itself changed versus what you changed manually.

A strong test suite makes it easy

This is non-negotiable: without comprehensive tests, Rails upgrades become guesswork. We've worked on applications with 30% test coverage and applications with 85% coverage. The difference in upgrade time and confidence is staggering.

Your test suite acts as your safety net. When you upgrade and something breaks, your tests tell you immediately and precisely where the problem is. Without tests, you ship to staging or production and discover issues the hard way.

At minimum, ensure you have:

  • Unit tests for business logic and models
  • Integration tests for critical user workflows
  • API endpoint tests if you expose an API
  • Test coverage for background jobs, caching, and external integrations

A practical example: imagine you're upgrading from Rails 6 to Rails 7 and ActiveRecord changes how it handles associations with custom scopes. Without tests, you might not discover this until a customer reports an issue in production. With tests, your suite catches it immediately.

Run your full suite after each step of the upgrade process:

ruby
bundle exec rails test
bundle exec rspec # if using RSpec
bundle exec parallel_test # if running in parallel for speed

If tests pass, you have confidence to proceed. If they fail, the failure message pinpoints the incompatibility. Then you fix it, run the suite again, and continue. This cycle takes longer upfront but is dramatically faster than debugging in production.

For teams that don't yet have solid test coverage, start building it before your next upgrade. Don't wait until you're in the middle of migrating. Allocate a sprint to testing infrastructure first. It pays dividends immediately and compounds over time.

Common mistakes

Mistake 1: Upgrading multiple versions at once. We see this frequently with teams that have deferred upgrades. Resist the temptation to jump from Rails 5.2 to 7.1 in one go. The debugging cost far exceeds the time you save by batching updates.

Mistake 2: Ignoring deprecation warnings. Deprecation warnings are your early warning system. They're never random noise. Each one points to a behavior change that's coming in the next version. Address them before you upgrade the version.

Mistake 3: Skipping the rails app:update command or merging it carelessly. This command exists for a reason. It updates your Rails skeleton to match the new version's expectations. Skipping it or carelessly accepting all defaults can leave your application in an inconsistent state. Review each change carefully.

Mistake 4: Attempting an upgrade without a safety branch. Always work on a dedicated feature branch. Don't attempt this on main or production. If something goes wrong, you can abandon the branch and start fresh without disrupting the team.

Mistake 5: Relying on external services to remain compatible. If you use third-party APIs, gems, or services, check their compatibility with your target Rails version before you commit to an upgrade. Sometimes a dependency needs an update too, or it's incompatible entirely. Budget time for this discovery.

Key takeaways

  • Upgrade one minor version at a time. Never skip versions.
  • Run your test suite before and after each upgrade to catch regressions early.
  • Fix deprecation warnings in your current version before bumping the Rails version in your Gemfile.
  • Use rails app:update to handle configuration file changes automatically, then review each change carefully.
  • Maintain sufficient test coverage so that you catch incompatibilities immediately rather than discovering them in production.
  • Build a regular upgrade cadence into your team's workflow. Small, frequent upgrades are easier than large, infrequent ones.
  • Document any custom configuration or monkey-patching so you understand what's yours versus what Rails provides.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles