Managing different tools in your modern application can be a pain. For example, defining the ruby version in your Dockerfile, CI/Deployment pipeline and local environment can sometimes means duplicating the definition in different files.
In One Version to Rule Them All,@hschne proposes a workflow to keep them all in sync. Follow the link to learn all the details, but here is his conclusion at the end:
Want to upgrade Ruby? Change .ruby-version. Want to upgrade Node? Change .node-version. Update pnpm? Change the version in your package.json. Any change will be picked up across all your environments. It’s simple, and it works beautifully.
What do you think? Have you faced a similar challenge? Do you use a different process?
I think that’s a good approach. I use nvm with .nvmrc for Node, but basically same idea. I don’t want a single “tool manager”, I want to use the best ones for each language…some projects are Ruby-only, some are Node-only, and only some are both. I like the idea of reading from the individual dotfiles whenever possible in other configs.
We take a different approach. Our whole fleet runs a single Ruby version in production (Passenger, not containers), so there’s no per-app version to sync.
CI runs two parallel jobs: one against $PRODUCTION_RUBY, one against $FUTURE_RUBY. My partner runs production locally, I run the future version. We maintain cross-compatibility continuously, so when the moment feels right, we flip PRODUCTION_RUBY=FUTURE_RUBY and advance FUTURE_RUBY to the next candidate.
Security patch releases aside, the server only needs one Ruby update per major version bump – no matter how many apps we have. The tradeoff is ongoing compatibility discipline rather than upfront containerization overhead, but with ~68 apps, that math works in our favor.
We use a .tool-versions file locally, but also have .node-version and .ruby-version files. mise works out really well across a wide variety of tools for us. We can use it to keep everybody on the same Postgres, Redis, Ruby, Node, and pnpm versions, which is super handy. We use Depfu for version management, and it’s pretty good at picking up all of the places a Ruby or Node version is used, so it handles updating all the places.
Thanks for sharing your approaches! This is very helpful.
@andynu That’s an interesting approach! For other tools like node or other dependencies do you use a similar approach or you define the version per project?
@mockdeep How do you keep the versions in those 3 files in sync when there is a change of version?
I’m a big fan of having a .tool-versions for local development and CI (personally I use asdf but mise is also supposed to be excellent) and I use a Dockerfile for production/staging but I tend to only lock down the major and minor, so I’ll use FROM ruby:4.0. This is so I get the latest patches and security updates every time I deploy but I am never jump scared by a breaking change.
I’ve seen people define the Ruby version in their Gemfile to ensure production, CI, and Docker all match and I don’t mind that but don’t personally do it.
I also use very similar rules for nodejs and have had good success with it.
I’ve actually had a crazy idea to use system Ruby, nodejs, etc and have no package manager at all for my programming languages. This is kind of born from the problem of globally installed gems/libraries/tools like ruby-lsp which can just “disappear” if I change directory plus it means the operating system will just do all my security updates for me. Then my Dockerfile can just be FROM debian.
We use Depfu for updating Ruby/Node versions and it automatically finds most common places versions are mentioned in the codebase, so we don’t need to manage the files ourselves.