Een Rails-app upgraden zonder alles plat te gooien
De eerste mail van de klant luidde: “We zitten op Rails 6.1. Onze senior developer zegt dat upgraden zes maanden gaat kosten en dat we al het feature-werk moeten stilleggen. Klopt dat?”
Ik hoor deze vraag in allerlei varianten een paar keer per jaar. Het antwoord is bijna altijd nee — maar de aanpak maakt een enorm verschil.
Na negentien jaar Rails heb ik tientallen apps geüpgraded. De meeste liepen achter. Sommige beschamend ver: Rails 4.2 in 2022, Rails 5.0 met een Gemfile vol gems gemarkeerd als git: 'some-abandoned-repo'. Geen van alle vereiste een big-bang-rewrite of een stillegging van zes maanden. Elke app werd stapsgewijs geüpgraded, in productie, terwijl de business gewoon doorliep.
Zo doe je dat.
Waarom big-bang mislukt
De reflex is om een branch aan te maken, Rails direct naar de nieuwste versie te bumpen, alles wat kapotgaat te fixen en te mergen als het werkt. Die branch leeft zes maanden. De main-branch blijft doorgroeien. Je upgrade-branch raakt steeds verder van de realiteit. Uiteindelijk ben je niet aan het upgraden — je bent aan het herschrijven. Je hebt twee dingen tegelijk geprobeerd: Rails upgraden én zes maanden main-branch inhalen. Geen van beide eindigt netjes.
Het alternatief is incrementeel upgraden: één minor versie tegelijk, op een kortlevende branch, gemerged naar main binnen een week. Je shipt misschien tien keer in plaats van één keer. Toch ben je sneller.
Stap nul: voordat je de Gemfile aanraakt
Twee dingen voor je ook maar één versienummer wijzigt.
Ten eerste: fix je testdekking. Je hebt geen 100% nodig. Wel dekking van de bedrijfskritieke paden — de flows die je klant geld opleveren of die, als ze kapotgaan, een golf van supporttickets veroorzaken. Als je geen tests hebt, schrijf ze nu. Zelfs 30% zinvolle dekking is beter dan blind vliegen.
Ten tweede: zet deprecation warnings aan en fix ze allemaal. Rails maakt dat eenvoudig:
# config/environments/development.rb
config.active_support.deprecation = :raise
Draai je testsuite. Draai de app lokaal een paar dagen. Elke deprecation warning is iets wat kapotgaat in de volgende Rails-versie. Fix het nu, terwijl je nog op de versie zit die begrijpt wat je fout doet.
In productie log je deprecations in plaats van ze te raisen:
# config/environments/production.rb
config.active_support.deprecation = :log
Bekijk je logs daarna. Je zult verrast zijn door wat er sluimert.
Het upgradepad
Rails hanteert een heldere major.minor.patch-versioning. Het ondersteunde upgradepad loopt via opeenvolgende minor versies:
6.0 → 6.1 → 7.0 → 7.1 → 8.0
Je kunt minor versies niet overslaan. Probeer niet in één keer van 6.1 naar 8.0 te springen — de cumulatieve deprecation-wijzigingen en framework-verschuivingen produceren een debuggingsloopgraaf. Elke minor-stap heeft zijn eigen gerichte wijzigingen, en de upgrade guides zijn echt goed. Gebruik ze.
Voor elke versiesprong:
- Update
gem 'rails', '~> X.Y'in je Gemfile - Draai
bundle update rails(alleen Rails, niet alles) - Draai
bin/rails app:update— dit updatet gegenereerde bestanden zoalsconfig/application.rb,config/boot.rben initializers - Bekijk elke wijziging van de generator met
git diff - Draai je testsuite
- Fix wat kapotgaat
- Zet de nieuwe framework defaults aan via de gegenereerde initializer
Die laatste stap is belangrijk. Rails levert nieuwe defaults in een apart initializer-bestand zodat je ze stapsgewijs kunt adopteren:
# config/initializers/new_framework_defaults_7_1.rb
# Gegenereerd door bin/rails app:update
Rails.application.config.action_dispatch.default_headers = {
"X-Frame-Options" => "SAMEORIGIN",
"X-XSS-Protection" => "0",
"X-Content-Type-Options" => "nosniff",
"X-Permitted-Cross-Domain-Policies" => "none",
"Referrer-Policy" => "strict-origin-when-cross-origin"
}
Pas niet blind alle nieuwe defaults tegelijk toe. Lees elke instelling. Begrijp wat er verandert. Pas toe, test, ship. Herhaal.
Gem-dependency-hell
Dit is het echte werk. Rails-upgrades worden niet geblokkeerd door Rails zelf, maar door gems die pinnen op oude Rails-versies.
Begin met een health check:
bundle outdated --strict
Voor elke gem die al jaren niet is geüpdatet:
- Controleer of de gem actief onderhouden wordt (GitHub-commits, RubyGems-downloadcurve)
- Controleer of er een onderhouden fork bestaat
- Controleer of de functionaliteit inmiddels in Rails core zit — het ecosysteem is de afgelopen jaren flink geabsorbeerd
- Als verlaten: vervang het, inline het, of verwijder het
Patronen die ik steeds tegenkom:
acts_as_paranoidofparanoia— vervang door de Discard-gem of een simpeledeleted_at-scope- Oude authenticatie-gems — Rails 8 heeft nu een volledige generator via
rails generate authentication paperclip— vervangen door Active Storage, al in Rails sinds 5.2- Legacy admin-gems gepind op oude Rails — evalueer of Administrate of een custom admin de weg vooruit is
Voor gems die niet helemaal compatibel zijn maar ook niet actief blokkeren:
# Gemfile — alleen als tijdelijke brug
gem "some-gem", github: "author/some-gem", branch: "rails-8-compat"
Gebruik git-dependencies alleen als tijdelijke maatregel. Pin aan een specifieke commit als je niet vertrouwt op de branch:
gem "some-gem", github: "author/some-gem", ref: "abc1234"
Markeer ze met een # FIXME: verwijderen zodra gem versie X.Y uitkomt-commentaar zodat ze niet eeuwig blijven hangen.
De valkuilen per versie
Rails 7.0: De grootste wijziging is dat Zeitwerk verplicht wordt voor autoloading. Als je app niet-standaard load paths of monkeypatching-trucs in initializers gebruikt, komen die hier boven. Draai bin/rails zeitwerk:check meteen na de upgrade. Het vertelt je precies wat niet automatisch geladen kan worden en waarom.
Rails 7.1: config.active_record.query_log_tags_enabled staat nu standaard op true. Dit voegt annotatiecommentaar toe aan je SQL-queries — uitstekend voor debuggen, soms verrassend als je tests hebt die asserties doen op rauwe SQL-strings. Ook before_action met if:-condities gedraagt zich subtiel anders rondom evaluatietijdstip. Bekijk je controller-callbacks als er iets onverwachts gebeurt.
Rails 8.0: Propshaft vervangt Sprockets als de standaard asset pipeline. Als je nog op Sprockets zit, heb je twee opties: opt out met gem 'sprockets-rails' en laat het werken, of migreer. De Propshaft-migratie is de moeite waard maar hoeft je Rails 8-upgrade niet te blokkeren. Verder zijn Solid Queue, Solid Cache en Solid Cable nu de standaard. Je hoeft ze niet meteen te adopteren — je bestaande Redis/Sidekiq-setup werkt gewoon nog — maar begrijp dat ze bestaan en evalueer op je eigen tempo.
De upgrade testen in productie
Ik geef sterk de voorkeur aan het deployen van upgrades op een canary-instantie vóór de volledige rollout. Met Kamal 2 gaat dat eenvoudig:
# config/deploy.yml
servers:
web:
hosts:
- 10.0.0.1 # canary
- 10.0.0.2
- 10.0.0.3
Deployen naar canary, vijf minuten je foutregistratie monitoren (Sentry, Honeybadger, wat je ook gebruikt). Als de foutrate niet stijgt, uitrollen naar de volledige fleet.
Heb je nog geen canary-deployments, deploy dan op zijn minst buiten piekuren en schrijf je rollback-procedure op vóór je begint. Een Rails-versie terugdraaien is doorgaans git revert + bundle install + herdeployen — snel als je voorbereid bent, chaotisch als dat niet zo is.
Hoe “klaar” eruitziet
Een voltooide Rails-upgrade heeft vier kenmerken:
bundle exec rails -vgeeft de doelversie terug- Alle deprecation warnings zijn weg — verifieer dit twee weken na de deploy door productielogs te checken
bin/rails app:updateis gedraaid en nieuwe framework defaults zijn bekeken en geadopteerd- De upgrade-branch is gemerged en verwijderd — er leeft geen “upgrade-branch” meer in je repo
Dat laatste telt meer dan het klinkt. Als de branch nog openstaat, is de upgrade purgatory, geen klaar. Sluit hem of gooi hem weg.
Het verborgen dividend
Er is een voordeel van actueel blijven dat in geen enkel ticket of sprint verschijnt: elke incrementele upgrade is kleiner en makkelijker dan de vorige. Het team dat Rails 8.1 shipt zes weken na de release, doet ongeveer tien minuten werk. Het team dat vier jaar niet heeft geüpgraded, doet zes maanden werk.
Een klant vroeg me ooit wat het zou kosten om hun Rails 4.2-app te upgraden. Mijn antwoord: ongeveer hetzelfde als wanneer ze elk jaar één keer hadden geüpgraded gedurende vier jaar. Alleen moesten ze nu die volledige achterstand inhalen in een gecomprimeerde tijdlijn, met alle zakelijke druk van dien en een productroadmap die niet kon pauzeren. De schuld rente-op-rente.
Blijf actueel. Je toekomstige zelf zal je dankbaar zijn.
Veelgestelde vragen
Hoe lang duurt één minor versie-upgrade?
Voor een goed geteste app met redelijk actuele gems: een halve dag tot twee dagen. Voor een legacy-app met slechte testdekking en veel verouderde gems: één tot twee weken. De testsuite en gem-dependencies zijn bijna altijd de bottleneck, niet Rails zelf.
Moet ik Ruby en Rails tegelijk upgraden?
Nee. Één tegelijk. Upgrade eerst Ruby — dat is meestal eenvoudiger en de foutmeldingen zijn duidelijker — deploy het, laat het een week in productie draaien en begin dan aan de Rails-upgrade. Beide wijzigingen combineren verdubbelt je debuggingoppervlak als er iets misgaat.
Wat als een cruciale gem verlaten is en er geen vervanger bestaat?
Drie opties: de relevante code inline in je app zetten (gems zijn vaak kleiner dan ze lijken als je de abstractie weghaalt), forken en zelf onderhouden (haalbaar als de gem stabiel is en weinig verandert), of iemand betalen die het domein begrijpt om een vervanging te bouwen. Ik heb alle drie gedaan. Inlinen gaat meestal sneller dan het klinkt en elimineert de dependency permanent.
Hoe ga ik om met database-schemacompatibiliteit tijdens een meerstaps-upgrade?
Je schema staat los van Rails-versioning. Migraties gegenereerd in Rails 6 draaien prima in Rails 8. Het enige aandachtspunt is als een migratie syntax gebruikt die Rails 8 heeft gedeprecated — maar de migratiebestanden zelf hoeven niet te worden aangepast. Richt je database-aandacht op ActiveRecord API-wijzigingen (query-interface, encryptie, strict loading), niet op de schemabestanden.
Moet ik alle nieuwe framework defaults meteen inschakelen?
Nee. De new-defaults initializer-bestanden zijn ontworpen voor geleidelijke adoptie. Zet één nieuwe default aan, test het en ship — en herhaal dat voor de volgende. Dit is de bedoelde werkwijze. Voel geen druk om alle schakelaars tegelijk om te zetten. Het bestand dat door app:update is gegenereerd, dient als je checklist; werk er op je eigen tempo doorheen.
Een legacy Rails-app upgraden en weet je niet waar de lijken begraven liggen? TTB Software werkt met Rails sinds versie 1.2. We vinden de blockers, ruimen het pad en brengen je app up-to-date — zonder je team stil te leggen.
About the Author
Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.
Get in TouchRelated Articles
Need Expert Rails Development?
Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise
Schedule a Free Consultation