As someone who deploys Ruby software to our servers, I've come up with a formula that's served us well.
We take all gems associated with the app (like a Rails app, but we do it with others), and stuff them along with the code into a package created by fpm.
In other words, the packages are completely self-contained; no gems are used from the environment.
The key to this is the --path option to bundle install, i.e.:
bundle install --path vendor/bundle
This command makes sure all my gems are in a sub directory of my project called vendor/bundle. When creating the debian package using fpm which includes all the source code, vendor/bundle also comes along with the package.
After installing the app on our Linux servers, 'bundle exec rails server' uses the gems from vendor/bundle.
This approach does require that I create the debian package on a machine that closely mirrors the deployed servers, so that compiled gems make any sense.
Most all the issues described in the article have not been problems because of this approach. I used to use rvm as part of my build process; it was very brittle. I'm completely happy now; just wanted to share in case that helps someone else.
>We take all gems associated with the app (like a Rails app, but we do it with others), and stuff them along with the code into a package created by fpm.
That is exactly what I've started doing at work, actually. It's a short-term win and rather simple, but it's a pretty big hack. Package bundling is quite poor form as it leads to file system duplication and for each duplication you have an additional location to patch when a security vulnerability is inevitably discovered. Ideally each Ruby gem would be its own package. I'm also concerned with reproducibility, so I build packages in as isolated of an environment as possible where I run 'bundle install' with a fresh vendor/bundle directory. Builds take longer than they should because of all the re-downloading. Using Guix, where I can easily represent each Ruby component in a package object, I can take advantage of its content-addressable storage to save time on repeated builds by using the cached gem builds from previous times.
We use a 'monorepo', so everytime we update any gem dependency, all of our apps get rebuilt entirely. So, updating all of our packages or not isn't a problem.
And I like the duplication, believe it or not. After having debugged environmental problems with dependencies in various languages over the years, I'm happiest knowing the dependencies in question are in vendor/bundle; end of story. Sometimes, in case of a nasty bug, I have a one-liner fix and I can go to vender/bundle, tweak the gem, and know I only affected the app using it. Those are things I prefer over some lost hard drive space.
Build times are a little long for us, though. That is true. I do a fresh bundle install on every build. That is the second longest part of the build, behind running our dog slow web tests.
The duplication is less of an issue in the corporate, one application per VM environment. However, my focus isn't solely on that. Web applications are really hard for most people to self-host. They are made a bit easier by things like OmniBus packages that bundle absolutely everything, which makes the user dependent on each application author to ship security fixes to software they didn't write.
There's also the issue of stateful package managers that can break in the middle of a package install and you're screwed. Most of the time that doesn't happen, but I've been bitten before. So the fpm approach is a great short-term win and I want to roll it out to the production systems at work soon, but in the long term I think we need functional package management to make our systems more resilient to failure.
We take all gems associated with the app (like a Rails app, but we do it with others), and stuff them along with the code into a package created by fpm.
In other words, the packages are completely self-contained; no gems are used from the environment.
The key to this is the --path option to bundle install, i.e.:
bundle install --path vendor/bundle
This command makes sure all my gems are in a sub directory of my project called vendor/bundle. When creating the debian package using fpm which includes all the source code, vendor/bundle also comes along with the package.
After installing the app on our Linux servers, 'bundle exec rails server' uses the gems from vendor/bundle.
This approach does require that I create the debian package on a machine that closely mirrors the deployed servers, so that compiled gems make any sense.
Most all the issues described in the article have not been problems because of this approach. I used to use rvm as part of my build process; it was very brittle. I'm completely happy now; just wanted to share in case that helps someone else.