Ruby on Rails upgrade service

It’s more work than a quick bundle update rails

Your problem

Your application is not on the latest version of Rails. In fact, it’s on a version that’s not actively maintained anymore.

Security risks

If you’ve got an application on Ruby on Rails 5.2 or older, then official security update support for this version has stopped since the summer of 2022. Any vulnerabilities found here won’t be patched anymore by the core team.

If you’re on version 6.0 then you’re only getting critical security updates. Minor security updates are not backported to this version, so they will affect you unless you manually patch your application.

Upgrading to the latest version, Rails 7.0, is recommended. For the sake of security you should at least upgrade to Rails 6.1.

Do you want to make headlines because an unpatched system led to a data leak and tarnished your good reputation with your customers?

Technical debt

Besides the security risks, not upgrading will have your application fall behind the ecosystem. A little bit is not an issue, and could even be an advantage by letting other folks discover bugs and incompatibilities. If you’re too far behind, though, then your team will start to notice. They will have to work with old versions of libraries, which increases friction by not being able to use current online documentation, and may have unexpected integration issues with other dependencies that assume the latest version. This causes bugs and wastes valuable developer time.

If you’re too far behind on Rails, then your team can’t install the latest version of other dependencies that integrate with it due to conflicts. This means they need to install outdated versions that are unsupported and thus come with additional technical debt of needing to upgrade it in the future. Congratulations: your old technical debt is generating more technical debt!

Opportunity cost

The last problem is the reason why you’re not on the latest version of Rails yet: for the business there is a very real opportunity cost attached to having your own experienced developers halt work on customers’ feature requests in order to do something that won’t actually help to grow the business.

Given that Rails upgrade can take quite some time if the application has other technical debt that needs to be dealt with first, when you do sit down for it the task could take weeks. Your customers, users or competitors might not give you the space to make this necessary investment in your infrastructure at the cost of delaying other work.

The solution

Lagging too far behind on Rails upgrades is bad, but sometimes it feels like you just don’t have the opportunity to do the right thing. How do you deal with this?

Upgrades benefit from experience

Upgrading Ruby on Rails has definitely gotten easier over the years, but it’s still a lot of custom work. It confronts you with your technical debt and demands you pay down most of it.

If it’s your first upgrade, you’re going to discover the pitfalls by falling into them. When you’ve done an upgrade multiple times you’ll have a better idea of the things to watch out for, so your experience makes you faster and it prevents problems.

Upgrading is a great onboarding task

Applying an upgrade requires mostly generic upgrade knowledge and only a little bit of domain knowledge that is unique to your application. This makes it a good onboarding job for an experienced developer who’s performed the upgrade before: it helps them learn how your application works while they apply a valuable upgrade.

Benefits of hiring a consultant

If don’t have a convenient new hire with relevant upgrade experience, then the next best thing is to hire a consultant who specializes in upgrades. This way your team won’t take a productivity hit, the work gets done faster, and you still get the needed upgrade. It only costs a some money that pays itself back while your developers keep growing your business.

If you’ve currently got an open job vacancy, then you have the budget to hire a consultant to handle your upgrade.

Our offer

Transparency reduces uncertainty

We are experienced in performing Ruby on Rails upgrades. We’ve got a process that helps catch most preventable issues, minimizing the risk of breaking things in production. During the upgrade you will always know where in the process we are, and what’s left to be done.

We’ve described the outline of our process below. You are free to have your developers borrow this approach (there is no magic, just sensible work) but we’re confident that the opportunity costs of doing this makes it much more cost effective to let us do it for you. This way they only need to spend a few hours making judgment calls and reviewing our work, rather than spending days or weeks on it themselves.

Small steps reduce your risk

Due to the custom nature of Rails applications, it’s impossible to give a precise estimate of how long any one upgrade will take. Sorry. While we have some rules of thumb, there are no guarantees. This makes upgrades scary from a budgeting perspective.

Traditional consulting contracts focus on delivering the entire update for either a fixed price or they charge you by the hour. Fixed price puts all of the risks on the consultant, which forces them to only work with pessimistic estimates to compensate for this risk. This means on average you overpay for fixed price contracts. When consultants charge by the hour, you run the risk of them “disappearing” for three months and then presenting you with a huge bill. This is the risk you want to avoid.

We recognize that both solutions are flawed due to focusing on delivering everything at once. It’s classic Waterfall Methodology.

Our approach is to have a clear process and take small steps that deliver results frequently. We’re usually never more than a day or two away from delivering more value, and advancing to the next step on our checklist. This approach strongly reduces your risks so we can charge by the hour, meaning you end up with a good price for good work.

Your budget informs pragmatic tradeoffs

Our process is basically a big checklist of things to do. Some of these tasks can result in more tasks: “check if other dependencies are up-to-date” can result in seeing that you’re totally up-to-date, or we find that you have 100 dependencies that are on average 2.5 years out-of-date. The latter will result in a task “update all other dependencies” which will likely result in a flurry of small updates and a few extra tasks for dependencies that will require a migration or total replacement. As we mentioned before, a Rails update confronts you with your technical debt and demands payment.

Your budget determines how far down the list of tasks we can go. Knowing your budget allows us to make pragmatic tradeoffs: see if we can apply a quick fix to turn a blocking large task into an optional one that we can move to the bottom of the priority list. This will allow more important things to be finished first.

For example: if you use a CMS dependency that is no longer maintained, it blocks the Rails upgrade. You would prefer to migrate to a different CMS that is maintained, but if this could potentially take more time than the budget allows it’s a really bad idea to do it now. If we can patch up the old CMS to make it compatible with the new Rails version, we can push the “migrate CMS” task far down the priority list and have enough time left to focus on the goal of upgrading Rails.

Experience and testimonials

We’ve used every single Rails version since 1.0.0. That’s over 16 years of Rails experience. We’ve developed and upgraded many different applications for our clients, and developed an upgrade process that works pretty well for us. Our clients have been happy with our work and often used the upgrade as a starting point for a retainer agreement to support them afterwards. We’ve got a great Maintenance Service for this.

Wes is a seasoned developer with solid knowledge of the Ruby and Ruby on Rails ecosystem. His healthy habits and critical yet positive eye brought more clarity to our codebase and processes, making developing more joyful for everyone. Wes is a good communicator, keeping track of important discoveries and decisions, asking input where necessary, pointing out possible improvements where sensible. And with him on board as a senior mentor, I’m sure our juniors will have what they need to make lasting contributions.

– Willem van Engen-Cocquyt, Lead Developer at Questionmark

Our offer to solve your problem

Your team has enough work in their backlog that only they are capable of doing. Hire us for your Rails upgrade, so your business growth won’t be delayed by necessary maintenance work.

Pricing follows the same rates as our Maintenance Service: because most upgrades will require 40+ hours to properly work through them, you always get our low rate of €90 per hour for Rails upgrades.

Because we offer multiple services, we try to keep our mornings available for routine maintenance and communication. We use our afternoons for uninterrupted development time on tasks such as your Rails upgrade. This means we work on upgrades for up to 20 hours per week.

As with all of our services, we have a Happy Client Guarantee. If you are not happy with the price/value ratio of what we have delivered, you can delete our changes and tear up the invoice. This is your guarantee that we won’t do anything but provide great value.

Note that our listed rates are excluding 21% VAT where required.

Email us to get your Rails application upgraded and become one of our happy clients.

Question: how much will it cost?

There’s a huge “it depends” factor here. In an ideal situation you’ve got all your technical debt cleaned up and the pre-upgrade preparations will only take about an hour to verify this. Afterwards the main upgrade usually takes up to 20 hours for major updates, or under 10 hours for minor updates. Add an extra 1-10 hours for post-upgrade cleanup. This means the optimistic scenario will typically take 12-31 hours with costs in the €1000-3000 range.

Compare this with a real case: two applications that were two major Rails versions behind, had most other dependencies 2+ years out-of-date, and had a few gaps in their test coverage that made upgrades riskier than desired. The client also wanted minor feature work done, and their budget was constrained. This gave about 120 hours for the upgrades, which turned out to be enough to do a major + minor Rails upgrade, add enough test coverage to make this responsible, perform minimal other gem upgrades that blocked the Rails upgrade, and left some time to evaluate options for the tricky ones. Multiple gem upgrades were done pragmatically, opting to tackle blocking tasks in ways that solved the immediate issue but left lower priority tasks, instead of going for perfect solutions that we simply did not have the time for. End result: the application was ready for another 2 years of Rails security updates, and now has Dependabot enabled to help the team catch up on the remaining updates. Note that the budget did not allow for the second major Rails update, nor for some gem migrations that would have been nice to do. Price for the upgrade of two applications: about €11k.

Another real case: three Rails applications deployed on the same machine with a shared database, of which two without test coverage. These needed a major and two minor Rails updates, as well as a minor Ruby version update. There were a few client-specific complications that made it take more time than a typical project. The three main upgrades followed my rule of thumb: about 15-20h for a major Rails version upgrade, 5-10h for a minor version upgrade. About 120h in total. Getting to the point where the upgrades could be applied was much more work, as we needed to create a CI pipeline with multiple database schemas and tooling to support this, and we ended up adding 90% test coverage from scratch for two of the applications, as well as solving some security-related issues discovered while adding the tests. The extra work added up to about 400 hours. Total time: 520 hours, so that’s about €47k.

Our Rails upgrade process

This is our upgrade process. You’re free to borrow it. If at any point you decide you’d rather have us do it then please email us.

Pre-upgrade preparation

Before you perform an upgrade, you want to make sure your application is ready for the upgrade. The goals are:

  • Was the previous upgrade completed fully? (i.e. are all configurations up-to-date, and are all deprecations resolved? see Post-upgrade cleanup below for more things you can check)
  • Are all non-Rails dependencies up-to-date? (You don’t want to have unexpected blockers)
  • Are there incompatible dependencies that need to be replaced first?
  • Is the current Ruby version compatible with the next Rails version?
  • Do you have enough automated test coverage to blindly trust your tests? If not, which parts of the application worry you?
  • Do you have monkeypatches that don’t have version guards? (It would be a shame if you don’t discover issues until production)
  • Does the new version add/remove frameworks you care about? Examples: if you intend to upgrade from v5 to v7 you might want to skip Webpacker. If you’re not interested in file uploads, then you can skip setting up, configuring and migrating ActiveStorage across each version.
  • Do you track errors in production? Some issues will only affect your production setup, so if you’re not tracking errors there you won’t know until your users/customers complain about it. Please use a service such as Honeybadger, AppSignal or Bugsnag to be aware of things that go wrong in production. Doing this way before the upgrade also helps identify pre-existing issues that are unrelated to the actual upgrade.

Note that you perform Rails upgrades one minor version at a time, without skipping steps. This allows you to follow the upgrade guides and benefit from gradual deprecation of features before they are removed or replaced. Reducing the number of things that change at a time helps to quickly find and resolve issues when they do occur.

How long the preparation takes depends on the application, and how you choose to deal with the obstacles we encounter. For example: the Paperclip gem is not compatible with Rails 6, with the suggested migration path being to migrate to ActiveStorage (which uses a very different approach to file storage and image transformation). Doing this migration could take days or even weeks depending on the amount of data you have and the features you need. Switching to the community-maintained kt-paperclip takes a minute and is compatible with Rails 6 and 7.

Each step should ideally be its own Pull Request (PR) against your stable branch. For upgrades it may be tempting to create one branch and do all the upgrade work on it, but if that branch ends up existing for a month or more you will have integration issues. In situations where you still end up with long lived upgrade branches, consider using a dual-booting strategy by using a gem such as Bootboot (which adds its own complexity so avoid it if you can).

Note that our Full Maintenance Service clients will have most of their ducks in a row, because it’s designed to get your application ready for upgrades and identify potential issues ahead of time. Other clients will be confronted with their technical debt and should expect to spend time on this accordingly.

Main upgrade

When your application is ready for the upgrade, it’s time to start. Our process, with each step consisting of one or more commits. Keeping changes small and well documented helps you discover where issues originated.

  • Upgrade the Rails version in Gemfile and run bundle update rails. It might be that you need to bump some other gems that are tightly locked to the Rails version and could not be bumped during the preparation.
  • Run rails app:update and accept all new/updated files. Then use source control to find what was changed compared to your version.
  • Process the changes in configuration: start with non-functional changes (such as updated comments, things that got moved) and end with functional changes (options that got renamed or removed, new options that got added). We tend to use this opportunity to rearrange settings to match the default order, to make future upgrades easier.
  • With a bit of luck you can now boot a console and/or run your tests.
  • Work through each failing test and other issues you encounter. If possible focus on one thing at a time. Did the upgrade change something? Was it mentioned in the changelog or Rails upgrade guide? If so was that by design (to remove insecure options, for example) or was it a mere change in default/preference? Or did things get renamed and do you have to do them in a different way now?
  • Continue until your application boots and your tests are green.

Congratulations, you’ve now done the minimum required to call this upgrade successful.

In our experience, if you’ve prepared properly, a minor Rails upgrade can be performed within 10 hours. A major Rails upgrade is usually done within 20 hours. That said, if you’ve got a codebase that’s much larger than a typical application or you tightly integrate with Rails internals that got overhauled, then this rule of thumb might be too optimistic.

Post-upgrade cleanup

Next up are the less obvious things you should do to call this migration complete:

  • Go through the Rails upgrade guide for the specific Rails version to address things that did not result in failing tests. These might only appear in production, or they might be subtle enough that you don’t have tests for this behavior.
  • Apply all new framework defaults, one at a time, where relevant and appropriate. When to apply some will be judgment calls that need to be coordinated with the team. For example: changes for cookie storage in Rails 6 from the Marshal to JSON format affects existing user login sessions. There is a migration path via Hybrid cookies, but the team will know best how long you’ll need that bridge solution to affect enough users to not cause inconveniences. Situations like this are why you upgrade one minor version at a time and deploy them, so you give yourself time to roll out these upgrades and observe side effects that you can then debug and fix or revert. Once you’re done, you can set config.load_defaults X.Y in config/application.rb to the new Rails version and delete the new framework defaults file.
  • Address all new deprecations as if they were errors. In fact, there’s a setting you can enable that does this for you: config.active_support.deprecation = :raise
  • Use a tool such as rubocop-rails to detect if new framework defaults can be applied that have been overlooked. ActiveRecord’s required/optional belongs_to declarations in Rails 5 that became mandatory in Rails 6 are a good example of this.
  • Migrate to new frameworks or tools that are unlocked on the new Rails version. Examples are system tests (which is a better version of feature tests in Rails 6) and replacing Webpacker with more modern JS/CSS handling tools such as import maps or jsbundling (in Rails 7).

During this process, having all tests pass will signal that things are good to go, unless it’s obvious that things only affect production environments. In that case, coordinate with the team to check if there would be an issue.

After upgrading Rails, it will be useful to look at updating Ruby to the latest version supported by your environment. Sometimes it just works, other times you need to plan that as an extra task because it involves a lot of changes.

This step is the most optional one, and easiest to postpone if you’re on a very tight budget.

Remember that you can email us to get your Rails application upgraded, or for a helping hand to get you through when you get stuck.