“The Web as I envisaged it, we have not seen it yet,” said Tim Berners-Lee, the inventor of the World Wide Web. “The future is still so much bigger than the past.”
He made that observation in 2009 – and since then, the web has grown substantially with no clear end in sight. This continuous evolution, though, presents a challenge: as tools and technologies advance, we often find ourselves maintaining codebases built on older versions. Ruby on Rails, once the industry’s new darling, has matured into a stable framework that still receives regular, significant updates.
The question, therefore, is how can we navigate a Ruby on Rails upgrade project as smoothly as possible? It’s a reasonable question. After all, Rails upgrades can feel intimidating – but with careful preparation, we can approach them systematically.
Here are ten steps we can take before embarking on our Rails upgrade journey.
Step 1: Assess Your Current Version
We need to know where we’re starting from. Let’s check our Rails version and note how many major versions behind we might be.
We can check our current Rails version by running:
rails --version
Or, if we need to check it programmatically within our application:
Rails::VERSION::STRING
Of course, Rails, like most Ruby projects, uses semantic versioning. This means that 1.2.3 would be major version 1, minor version 2, and patch version 3.
Major version upgrades are typically the most challenging, with minor versions often including some breaking changes but generally being more manageable. In many cases, we can upgrade to a later patch version without significant difficulty.
When planning our target upgrade version, we should keep an eye on future releases; what seems current today may age quickly. We can check the current supported release series at the Ruby on Rails site:
https://guides.rubyonrails.org/maintenance_policy.html
Even while planning this upgrade, it may be worth considering the next one. How long do we expect to go before upgrading again? Can we slot it onto our development calendar now? Thinking about the long-term maintainability of our codebase will help us make decisions that won’t need immediate revision.
Step 2: Catalog Your Customizations
We should make a comprehensive list of any monkey patches we’ve added. These are likely to break during the upgrade and will need special attention.
Are any of these customizations replaceable by built-in Rails features? If not, can they be replaced by a gem or other functionality? In many cases, it can be easier to remove old monkey patches and replace them with more mainstream approaches than to rewrite them for a newer version of Rails. This isn’t always straightforward, though – some customizations may represent unique business logic that we need to preserve carefully.
Step 3: Evaluate Your Test Coverage
Automated testing is crucial for a smooth upgrade. We should assess the coverage and quality of our existing test suite. If necessary, we may need to improve it before proceeding. While it’s technically possible to proceed without a fully functioning test suite, it makes the process more difficult and can complicate troubleshooting significantly.
If we do proceed without comprehensive test coverage, it’s important to document what portions are functioning before we begin. In some cases, developers have spent considerable time trying to debug test failures they attributed to a Rails upgrade – only to discover the tests were already broken or intermittently failing.
Some older Rails projects have dependencies on test execution order – and since this may change during the upgrade (newer Rails versions often use randomized test order by default), we should carefully note whether our tests can run out-of-order or only in sequence, and whether they can be run partially or require the full suite.
To assess our test coverage, we might run:
# For RSpec
bundle exec rspec --profile
# For Minitest
bundle exec rails test --verbose
Considering the long-term implications: a robust test suite will not only help with this upgrade but will make future maintenance and upgrades much smoother.
Step 4: Set Up a Staging Environment
Of course, we should create a dedicated staging environment to perform the upgrade without affecting production. If we already have a staging environment, we might consider setting up a separate one specifically for the Rails upgrade branch; it’s not uncommon to need to test and release other software changes during a Rails upgrade. We should do our best to keep our staging environment as similar to production as possible – this includes matching Ruby versions, database versions, and system configurations where feasible.
Step 5: Choose an Upgrade Strategy
We need to decide between an “all-in” approach (upgrading directly to the latest version) or an incremental approach (upgrading one or two versions at a time). This depends on the size and complexity of our application. Incremental approaches often take more overall time but can work more smoothly alongside parallel development on our main codebase.
The “all-in” approach can be faster in total calendar time but carries higher risk of encountering multiple breaking changes simultaneously. The incremental approach spreads out the work but requires careful management of deprecation warnings and intermediate states. Neither approach is universally “better” – it depends on our team’s capacity, the application’s complexity, and our tolerance for risk. We should also consider how long we can maintain multiple version branches if we choose incremental upgrades.
Step 6: Create a Detailed Upgrade Process Map
We should create a detailed roadmap for the upgrade process, breaking it into manageable milestones. For example, we might split it into phases like: resolving Gemfile conflicts, passing unit tests, passing functional tests, passing integration tests, and then manually testing our system by functional area.
This roadmap should include not just technical milestones but also time estimates, dependencies between tasks, and criteria for deciding whether to proceed to the next phase or pause for additional investigation.
Step 7: Notify Your Stakeholders
We should communicate the upgrade plan and timeline to all relevant parties – this includes our development team, leadership, customers, and any other stakeholders. We need to set clear expectations around potential downtimes, maintenance periods, and what functionality might be temporarily unavailable during the upgrade process.
Step 8: Practice, practice, practice.
The deployment process often involves multiple components; it may also require system administration changes, database upgrades, data migrations, and other modifications. We should plan on practicing the entire process – or, at least as much as is reasonable. For example, we might take an extra server and run the entire deployment routine on a copy of our live dataset – which can give us insight into what kinds of problems we might encounter. Many upgrade projects have been derailed by issues arising from differences between development, staging, and production environments – and careful rehearsal can help us minimize or even avoid these altogether.
We should consider creating a detailed checklist of deployment steps:
- Database backup procedures
- Environment variable configuration
- Asset compilation
- Cache clearing
- Background job queue management
- Monitoring setup
We should practice these steps in a staging environment that mirrors production as closely as possible.
Step 9: Have a Rollback Plan
Despite our best efforts, issues can still arise. We should have a detailed rollback plan ready in case critical problems are encountered. This plan should include:
- What backups need to be taken prior to deployment to ensure rollbacks can happen?
- Are any data migrations irreversible? If so, how will we handle them?
- Determining the “point of no return” where the upgrade can’t be reverted.
- If we’re moving to new infrastructure (cluster, cloud, server, or location), at what point can we safely free or reuse the old resources?
- What monitoring needs to be in place during the transition period?
- Who makes the decision to revert or not revert?
- If the decision is made to revert, who will implement it and how quickly can they do so?
Step 10: Take a Moment to Breathe
Although upgrade projects can feel daunting, they are, in the end, a series of hurdles to overcome. To be sure, it can be many hurdles, but many represent relatively straightforward changes. After finalizing our upgrade plan, we should take a moment to breathe – once we have the path mapped out, all we have to do is keep moving forward one step at a time.
Conclusion
By following these ten preparation steps, we can lay a solid foundation for a successful Rails upgrade. Remember that upgrades are fundamentally about long-term maintainability – the effort we invest now will pay dividends as we continue to develop and maintain our application.
Stay tuned for more guidance as we embark on our upgrade journey.
Resources
Here are some tools that can help with Rails upgrades:
- nixpkgs-ruby – Bob Vanderlinden’s collection of Nix code for older versions of Ruby can be particularly useful for getting legacy Ruby code running on newer operating systems.
- lock-gemfile – Many legacy Ruby projects don’t have version numbers pinned for some or all of their gems. The lock-gemfile tool can help with this – it will rewrite our Gemfile using the version numbers from our Gemfile.lock. This allows us to change a few versions at a time instead of having to resolve everything at once.
- mise – Since we’ll likely need to switch back and forth between different versions of Ruby, a version manager becomes essential; mise is a solid option, but we can also use asdf, rbenv, rvm, or other alternatives.
- direnv – direnv helps manage per-project environment variables using .envrc files; in particular, the “layout ruby” command can be helpful for upgrade projects, as it transparently stores our gems in a per-project .direnv/ruby directory. This can make switching between projects or branches using different Ruby versions much easier.
- ack – ack is a grep-like tool optimized for source code. It’s faster than grep, recursive by default, automatically ignores binary files and our .git directory, and has color highlighting by default. Since upgrade projects often involve extensive searching, ack can be quite useful.

