Upgrading Ruby 3.x to 4.0.2 and Rails 8 to 8.1.2
Edu Depetris
- Mar 18, 2026- Ruby
- Ruby On Rails
- Ruby 4
- Rails 8 1
- Rails Upgrade
We have an app that hasn’t been touched for the last 10 months.
Even though the Rails guides recommend upgrading only when there’s a good reason, and I don’t really have one, I’ll do it for the love of the craft.
I’ll go a bit further and also upgrade Ruby to the latest stable version. At the time of writing, that’s Ruby 4.0.2.
Let's go!
TL;DR
Upgrade worked, but required:
- Adding removed stdlib gems (cgi)
- Updating dependencies (drb, rubocop)
- Fixing a Bundler bug on Ruby 4.0.2
- Adding system deps (libyaml-dev) for Docker
Ruby time
To upgrade Ruby, I first need to update the list of available versions. In my case, I’m using rbenv, so these commands will do the job:
brew update brew upgrade rbenv ruby-build
Now let’s install the new version of Ruby:
rbenv install 4.0.2
These are the minimum changes we need in a default Rails app:
Dockerfile:
ARG RUBY_VERSION=4.0.2
.ruby-version:
4.0.2
Before installing the gems, note that in Ruby 4.0.0, as this link describes, the cgi gem was removed from the default gems and needs to be installed separately.
Gemfile:
# CGI was removed from standard library in Ruby 4.0 gem "cgi"
Now we’re ready to install the gems:
bundle install
Let’s see how it went by running the tests. We get some deprecation warnings and a few failing tests… time for a bit of Claude investigation.
gems/4.0.0/gems/railties-8.0.2/lib/rails/initializable.rb:3: warning: tsort was loaded from the standard library, but will no longer be part of the default gems starting from Ruby 4.1.0 You can add tsort to your Gemfile or gemspec to silence this warning. ruby/gems/4.0.0/gems/drb-2.2.1/lib/drb/drb.rb:367: warning: ObjectSpace._id2ref is deprecated
For the first warning, adding
tsort to the Gemfile does the job. For the second one, according to this, we just need to update the gem:
bundle update drbAfter that, our tests are clean:
bin/rails test ≈ tailwindcss v4.1.6 Done in 114ms Running 176 tests in parallel using 10 processes Run options: --seed 13208 # Running: ................................................................................................................................................................................ Finished in 3.247352s, 54.1980 runs/s, 159.5146 assertions/s. 176 runs, 518 assertions, 0 failures, 0 errors, 0 skips
Now that Ruby is upgraded and everything is green, let’s move on to Rails.
Rails Time
Let’s update the Rails version in the Gemfile and run:
gem "rails", "~> 8.1"bundle update railsbin/rails app:update
Here’s a quick summary of the changes (thanks Claude):
The upgrade is complete. Here's a summary of all changes: Core upgrade: - Gemfile — Rails ~> 8.0.0 → ~> 8.1.0 - config/application.rb — load_defaults 8.0 → 8.1 - Gemfile.lock — Rails 8.0.2 → 8.1.2 (+ dependency updates) Config updates from app:update (kept our customizations): - config/environments/development.rb — added verbose_redirect_logs and assets.quiet - config/environments/production.rb — minor formatting; SSL + Solid Cache/Queue restored - config/initializers/content_security_policy.rb — added report_only example - bin/rubocop, bin/setup — minor updates - public/*.html — updated error pages New Rails 8.1 files: - bin/ci + config/ci.rb — local CI runner (new feature) Preserved as-is: - bin/dev — kept Foreman-based for Tailwind watcher - config/environments/test.rb — kept autoload_paths for test helpers - config/puma.rb — unchanged Removed: - 3 unnecessary Active Storage migrations (no AS tables exist) - new_framework_defaults_8_1.rb (all defaults enabled via load_defaults 8.1) All 176 tests pass, production boots fine. Go ahead with your changes — let me know when you're ready to commit.
The upgrade went smoothly overall, but there were a few extra things to sort out.
Some extra things
Rubocop
RuboCop needs to be updated:
bin/rubocop RuboCop supports target Ruby versions 3.3 and above with Prism. Specified target Ruby version: 4.0
Fix:
bundle update rubocopDB seed:replant
Running
bin/ci
I got an
SQLite FK constraints.error when executing db:seed:replant
Fix:
RAILS_ENV=test bin/rails db:drop db:create db:schema:loadBundler Issue on Ruby 4.0.2
Running bin/ci resulted in:
cannot load such file -- .../bundler-2.5.16/lib/gems/bundler-2.5.16/exe/bundle (LoadError)
and some warnings:
/gems/4.0.0/gems/bundler-2.5.16/lib/bundler/rubygems_ext.rb:290: warning: already initialized constant Gem::Platform::JAVA /4.0.0/rubygems/platform.rb:279: warning: previous definition of JAVA was here /gems/4.0.0/gems/bundler-2.5.16/lib/bundler/rubygems_ext.rb:291: warning: already initialized constant Gem::Platform::MSWIN /4.0.0/rubygems/platform.rb:280: warning: previous definition of MSWIN was here /gems/4.0.0/gems/bundler-2.5.16/lib/bundler/rubygems_ext.rb:292: warning: already initialized constant Gem::Platform::MSWIN64 /4.0.0/rubygems/platform.rb:281: warning: previous definition of MSWIN64 was here /gems/4.0.0/gems/bundler-2.5.16/lib/bundler/rubygems_ext.rb:293: warning: already initialized constant Gem::Platform::MINGW /4.0.0/rubygems/platform.rb:282: warning: previous definition of MINGW was here /gems/4.0.0/gems/bundler-2.5.16/lib/bundler/rubygems_ext.rb:294: warning: already initialized constant Gem::Platform::X64_MINGW /4.0.0/rubygems/platform.rb:284: warning: previous definition of X64_MINGW was here /gems/4.0.0/gems/bundler-2.5.16/lib/bundler/rubygems_ext.rb:296: warning: already initialized constant Gem::Platform::WINDOWS /4.0.0/rubygems/platform.rb:286: warning: previous definition of WINDOWS was here /gems/4.0.0/gems/bundler-2.5.16/lib/bundler/rubygems_ext.rb:297: warning: already initialized constant Gem::Platform::X64_LINUX /4.0.0/rubygems/platform.rb:287: warning: previous definition of X64_LINUX was here /gems/4.0.0/gems/bundler-2.5.16/lib/bundler/rubygems_ext.rb:298: warning: already initialized constant Gem::Platform::X64_LINUX_MUSL /4.0.0/rubygems/platform.rb:288: warning: previous definition of X64_LINUX_MUSL was here ...
The root cause:
Bundler 2.5.16 constructs a malformed load path on Ruby 4.0.2, causing
bundle check/install to crash with a LoadError.Rails binstubs (`bin/rails`, `bin/rubocop`) still work because they boot through a different code path.
If you run Rails binstubs (bin/rails, bin/rubocop) work fine because they boot through a different code path.
The Fix:
Upgrade bundler from 2.5.16 to 4.0.8
After upgrade that
Docker build
Finally, let’s build the Docker image and check everything:
In my case, psych gem couldn't compile — libyaml-dev was missing from the Docker build stage.
Adding libyaml-dev to the build-stage fixes the issue:
After upgrade that
bin/ci works properly!Docker build
Finally, let’s build the Docker image and check everything:
kamal build devIn my case, psych gem couldn't compile — libyaml-dev was missing from the Docker build stage.
Adding libyaml-dev to the build-stage fixes the issue:
diff --git a/Dockerfile b/Dockerfile
index 8608602..1b895d2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -30,7 +30,7 @@ FROM base AS build
# Install packages needed to build gems
RUN apt-get update -qq && \
- apt-get install --no-install-recommends -y build-essential git pkg-config && \
+ apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Install application gemsAfter that:
kamal build dev ... INFO [cb10e153] Finished in 71.702 seconds with exit status 0 (successful).
That’s it — everything is now running on Ruby 4.0.2 and Rails 8.1.2 without issues.
Happy clauding! 🤖