Devot Logo
Devot Logo
Arrow leftBack to blogs

How to Successfully Upgrade a Massive Rails App from 4.2 to 6.0

Jerko Č.10 min readMay 28, 2025Technology
Jerko Č.10 min read
Contents:
Introduction: The challenge of upgrading Rails
The initial approach: Following official guidelines
Turning point
A new approach: Starting from scratch
Did it work?
Lesson learned: always start with a clean, working state
Step-by-step process
Pros of the new approach
Potential consequences of the new approach
Final thoughts

Introduction: The challenge of upgrading Rails

Some time ago, I had been working on a massive Rails 4.2 project, and eventually, I took over it. At that time, Rails 4.2 had reached its end of life, and Rails 6.0 had just been released.

I knew upgrading Rails was the only way forward, but the sheer size of the project and the fact that all previous attempts by more experienced coworkers had failed weighed heavily on me, especially since this was my first time tackling an upgrade. Not to mention, the frustration voiced by my coworkers during previous failed upgrade attempts only added to the pressure.

The mere thought of it filled me with a sense of unease.

However, with no other options available, it was my turn to tackle the challenge, so I decided to give it a shot.

The initial approach: Following official guidelines

I began by reviewing the official upgrade notes, setting my sights on upgrading to Rails 5.0 first.

However, what seemed like a straightforward task quickly turned into a nightmare. After spending an entire week on the upgrade, I realized it was nearly impossible to get the project running.

Following the official guidelines, I encountered error after error. The problem was that once I fixed one issue, another would pop up, only for the original error to resurface once again. Trying to start the Rails console, server, or run tests felt like an impossible mission, a never-ending cycle of confusion. I found myself completely lost, unsure of where the errors were coming from or how to resolve them.

Turning point

I spent an entire week, five full days, on the upgrade, but my progress was essentially zero. I hadn't moved an inch from the starting point. Considering that even my more experienced coworkers had also failed, it became clear to me that this approach wasn’t going to work!

So, I began brainstorming alternative solutions.

A new approach: Starting from scratch

One idea that came to mind was: what if I started from scratch?

I considered setting up a clean Rails 5.0 installation with no existing code and building the project from the ground up. The goal would be to have a working Rails app right from the start.

Then, I could copy over parts of the existing code, testing after each copy to see if it works or if errors would pop up. If an error occurs, it would be much easier to pinpoint the exact part of the code responsible, making it more straightforward to fix. Once I resolved an error and got a new portion of the codebase working, I could move on to the next part, repeating the process until the entire codebase was transferred over.

Well, I decided to give it a try!

I started by initializing a new, empty Rails project. However, instead of using Rails 5.0, I went ahead and set up the latest version, Rails 6.0. My original project was on Rails 4.2, so I decided to skip directly to Rails 6.0, bypassing versions 5.0, 5.1, and 5.2 altogether. For the rest, I followed the approach described above.

Did it work?

Actually, it worked!

It took me three full weeks to complete it, but it worked!

Considering that with the first approach, I hadn’t made any progress after a full week, this new approach turned out to be the bulletproof solution.
Keep in mind that the project was massive. It included four different APIs, a frontend, and a few thousand RSpec tests, factors that only reinforced the effectiveness of this approach.
Also, instead of upgrading version by version three times, it was done all in one go, directly to the latest version.

And all of that in just three weeks, the result was quite impressive.

Lesson learned: always start with a clean, working state

After that experience, I’ve never changed the way I approach upgrading Rails projects. No matter how big or small the project is, or whether it’s been properly maintained or not, I always start from scratch with a clear and clean working state.

First, initialize a new Rails project, install all required gems, and then copy and paste parts of the codebase. After each copy-paste, double-check file changes, check if everything works and repeat the process until everything is in place.

It works flawlessly every time. I’ve successfully used this approach for upgrades from 4.2 to 6.0, from 5.1 to 7.0, from 6.0 to 8.0, and from 7.0 to 8.0, each time, with perfect results.

Step-by-step process

After going through the process multiple times, I figured out the most effective approach for me. Here’s a set of steps with recommendations that I typically follow.

Initialize the new Rails project

Start by initializing a new Rails project. It’s recommended to use the latest Rails version unless certain gems restrict you.

You can initialize it in a new directory or within the same repository, depending on whether you will follow the approach outlined in the section below.

When setting up the new Rails project, ensure it mirrors your existing project as closely as possible.

Example:

If you are using the PostgreSQL database, then you need to use appropriate flags in the rails new command.

This ensures that your new Rails project is configured to use PostgreSQL out of the box, with the necessary configuration files set up for you.

By aligning the setup as closely as possible to your previous project's setup, you'll avoid discrepancies later on and make the whole process smoother.

Eager loading

After initializing a new Rails app, navigate to config/environments/development.rb and set eager_load to true.

Eager loading is useful for early detection of issues because it forces Rails to load all of your application's code at startup. This can reveal problems like missing files, syntax errors, or issues with dependencies that might otherwise go unnoticed until the specific part of the code is needed. In development, eager loading helps catch these errors early, allowing you to fix them before they lead to runtime issues or bugs that might be harder to track down later.

By loading everything upfront, it ensures that your code is fully initialized and ready to run without surprises, which is especially valuable in our case.

Tracking file changes

There are several tools available to track file changes, but regardless of which one you use, the most important thing is being able to clearly see the changes you're making.

For me, it is important to see:

  • The old Rails codebase

  • The new Rails codebase

  • File changes between the old and new Rails codebase

  • File changes added on top of the new Rails codebase (after initializing the new Rails app)

Example:

Here is an example of how I am doing that using git and git worktrees.

First, I need two branches:

  • master branch

    • It is the source of truth

  • rails-upgrade branch

    • Created from master branch

    • All changes related to the Rails upgrade will be made in this branch.

To work with both branches simultaneously, I use Git worktrees, which I always rely on. I already have a worktree set up for the master branch, and I created an additional worktree for the rails-upgrade branch.

  • master worktree - for master branch

  • rails-upgrade worktree - for rails-upgrade branch

To easily track all the changes mentioned above, I follow these steps:

  1. On the rails-upgrade branch delete the entire codebase/project and commit this change as 'RAILS UPGRADE.'

  2. On the rails-upgrade branch, initialize a new Rails app as outlined in the previous section and commit it as 'Initialize new Rails app.'

  3. All changes related to the Rails upgrade are made in the rails-upgrade branch. For each section below, implement the required changes, copy and paste from the master branch, and so on. Once you've completed a section, commit the changes and then move on to the next section.

  4. Repeat the previous step until the entire codebase has been upgraded.

  5. Rebase the rails-upgrade branch against the master branch and either fix up or squash all commits after 'RAILS UPGRADE' (I prefer to fix up)."

  6. That is it.

Following these steps will allow you to track the changes between your old and new codebase, as well as any modifications you make on top of the new Rails version. By using rebase with fix-ups, you can remove unnecessary changes (like deleting the entire codebase), ensuring that your pull request looks like a typical Rails upgrade.

Additionally, utilizing Git worktrees makes it easier to inspect or copy the codebase from the master branch without needing to check out between branches, significantly speeding up the entire process.

Gems used by project

Different versions of Rails come with different gem setups, so you'll need to manually select all non-Rails-specific gems used by your project. For each gem, check the latest version and review any breaking changes between the current and newest versions.

If there are breaking changes, check what they are and if they are easy to fix. In most cases, they are easy to fix.

The worst-case scenario is that the new gem version has a breaking change, and the current version isn’t compatible with the new Rails version. In this case, you'll need to invest more time refactoring, fixing, and testing the code related to the gem’s breaking changes.

For each gem, after installation, refer to the official documentation for proper setup to ensure it’s correctly installed and configured.

After installing and setting up each gem, test your application by starting both the Rails console and the Rails server. This will help confirm that your app is working correctly at every stage.

Once everything is in place, commit all changes as “Install and setup Gems”

Config directory

Some of these files are Rails-specific, while others were added in the previous step as part of the gem setup. For the remaining files, most of it was straightforward copy-pasting, except for the environment files. For these, manually review the old environment files, compare them with the new ones, and update the new environment files as necessary. After each copy or modification, carefully review the file changes and verify that the Rails console and server start without errors. If they start successfully, proceed to the next step; if not, resolve the issues first.

Once everything is in place, commit all changes as “Configuring app”

Bin directory

Initializing a new Rails app provides you with a fresh set of files. In most cases, you won’t need to modify anything unless you're dealing with gem-specific or custom-created files. For gem-specific files, it's best to regenerate them using the gem itself. For custom-created files, copying and pasting them with minor modifications, if necessary, will be enough.

Lib directory

When you initialize a new Rails app, the directory is typically empty. If it’s not, copying and pasting should be sufficient. However, if there are parts of the code, such as rake tasks, that depend on code from the app directory or other parts of the app, it’s best to address these last.

Inside the app directory

I usually start with the models, as they are the most independent from the rest of the codebase. Next, I move on to serializers, followed by routes, controllers, views, and so on, continuing until the entire directory has been transferred.

Sometimes, I copy file by file, and sometimes, the whole directory (for example, models, serializers, etc.). After copying, check if there are some important changes in files and if something has to be refactored or fixed. After that, check if the rails console and rails server will raise any errors. If yes, fix them until it works.

At this point, you can also start copying tests. For example, once you copy all models and make it work, you can copy and paste all tests for models and check that all tests pass. Repeat the process until everything is in place and works. Once you are done with section of code (example: models) fell free to commit it.

Last one

Check the remaining directories, including the root directory, for any missing files, such as the README or, in most cases, Docker files, and so on.

Also, remember to revert the eager loading setting to false in the development environment.

Once everything is working locally, you’re mostly done with upgrading itself!

The next steps, of course, include deployment, quality assurance, and so on, but these are not directly related to the Rails upgrade process we’re discussing here.

Pros of the new approach

Let's see several reasons why this approach works:

  • Starting from a clear and clean, working state (this is crucial)

    • Starting from scratch meant I had a new, clean, working app with no leftovers, legacy code, bad configuration, or setup. It is a solid foundation for building the rest of the app.

  • The whole project has been set from scratch

    • Starting from scratch meant all libraries with their setups were done from scratch. So, no dealing with leftovers, legacy code, bad configuration, or bad setup. Especially helps when there are breaking changes between versions.

  • No leftover code from previous Rails versions

    • There are no leftovers, neither due to rails itself, libraries, or anything.

  • Clear and measurable progress

    • With each copy-paste-test-fix-until-it-works iteration, you have code that works. Once it works, it will work for the rest of the process. No need for returns back to it.

  • No ambiguous errors

    • Adding a new part of code on already working code with a clean state will not cause ambiguous errors that will resurface back again after a few more steps.

  • Easily pinpoint which part of the code is causing issues

    • Adding new code to existing working code makes it very easy to pinpoint which part of the code is causing issues

  • No need to upgrade version by version (very important)

    • With this approach, there is no need to upgrade version by version. You can skip from any version to any version. You can even do downgrades!

  • A calm mind (believe me, this one is very, very important)

    • No worrying about forgetting something

    • No frustration or headaches

    • You feel a very high level of confidence about progress and overall work

Potential consequences of the new approach

I’ve really tried to come up with a solid downside, but honestly, I can’t think of many.

Well, maybe the only drawback is the sheer amount of copy-pasting, starting up the rails console and rails server. However, that is a small price to pay for the clarity and control it gives you over the process.

Another consideration that comes to mind is if you're working in a large team with active development. In that case, the upgrade process can become more iterative as you’ll need to reapply any changes made after you started the upgrade. There are two ways to handle this. The first is using git rebase, but when there are too many changes, I tend to avoid it. Instead, I review each commit, manually reapply (copy-paste) the necessary changes, and then perform a git rebase. In the case of any conflicts, just accept all incoming changes.

Final thoughts

If you're struggling with a Rails upgrade, consider starting from a clear and clean working state. While it might seem unconventional, it will ultimately save you time, reduce frustration, and help you avoid countless headaches, ensuring you upgrade with confidence.

Happy Rails upgrading!

Spread the word:
Keep readingSimilar blogs for further insights
How Figma Dev Mode Improves Collaboration Between Designers and Developers
Technology
Luka C.6 min readJun 18, 2025
How Figma Dev Mode Improves Collaboration Between Designers and DevelopersFigma Dev Mode helps frontend teams build faster and more consistently by bridging design and code. Learn how early collaboration and reusable components improve workflow quality.
Next.js vs React: Which One Is Right for Your Next Web Project?
Technology
Mario F.4 min readJun 11, 2025
Next.js vs React: Which One Is Right for Your Next Web Project?Next.js vs React isn’t just a framework debate, it’s a decision that shapes performance, SEO, and development speed. This guide breaks down their key differences to help you choose the right tool for your next project.
Spring Data REST: Simplify Java Development with Automated RESTful APIs
Technology
Tomislav B.6 min readJun 4, 2025
Spring Data REST: Simplify Java Development with Automated RESTful APIsSpring Data REST is a powerful framework that speeds up Java development by automatically generating RESTful APIs from Spring Data repositories. This blog explores its key features, provides practical examples, and offers best practices for efficient API development.