Using Nix to manage multiple Ruby versions

What is Nix

Nix is many things, fundamentally it's a build tool and a language: as a build tool it focuses on purity and predictability; as a language it focuses on being declarative; more generally it is a package manager and an operating system. Today I'm going to be talking about using it as a package manager.

The ruby version problem

Having multiple projects that run multiple different versions of Ruby can be a problem, and isn't unique to just Ruby. The reason it's a problem is that each version of the language adds new features or deprecates methods people use. If we have a single version of Ruby installed on a development machine, we might be using a different version of Ruby than our project will use in production. This problem is so common that a number of tools have been created to deal with it such as RBenv and RVM. However, Nix provides us with a new alternative solution.

How Nix solves it

Nix gives us a way to create separate isolated development environments. This means I don't have any version of Ruby globally installed on my machine. Instead each and every Ruby project contains a default.nix file in it's root, which defines all of the dependencies that project needs to run.

Here is one such example:

  pkgs = import <nixpkgs> {};
  stdenv = pkgs.stdenv;

  ruby = pkgs.ruby_2_2_3;
  rubygems = (pkgs.rubygems.override { ruby = ruby; });

in stdenv.mkDerivation rec {  
  name = "mortgages";
  buildInputs = [

  shellHook = ''
    export PKG_CONFIG_PATH=${pkgs.libxml2}/lib/pkgconfig:${pkgs.libxslt}/lib/pkgconfig:${pkgs.zlib}/lib/pkgconfig:${pkgs.mysql}/lib/pkgconfig:${pkgs.imagemagickBig}/lib/pkgconfig
    export C_INCLUDE_PATH=${pkgs.libmysql}/include/mysql

    mkdir -p .nix-gems
    export GEM_HOME=$PWD/.nix-gems
    export GEM_PATH=$GEM_HOME
    export PATH=$GEM_HOME/bin:$PATH


In this file not only do we specify the Ruby version (v2.2.3) that the project requires to run, but we also specify all of the native dependencies that some of our Ruby Gems require to run.

Inside the let we are simply defining some aliases and then overriding the rubygems package to ensure we're using the same ruby version everywhere. Inside mkDerivation is where we start to build our development environment, name is the name of the project we're inside, buildInputs is where we define the system dependencies.

shellHook is basically a shell script that will run at the very end where we can do whatever we want. We have to do a couple of things using shell because of how isolated Nix makes everything by default. Installing dependencies such as libxml2 doesn't make it available and easy to find by the Gems that require it, so we set PKG_CONFIG_PATH to make this possible. The other thing we do is create a new directory and set environment variables to change where Gems are installed. We do this to preserve isolation of projects, otherwise Gems might get installed into a shared location which could cause unexpected behaviour.

In Action

Globally I don't have Ruby installed:

[~/code/mortgages] $ ruby --version
The program ‘ruby’ is currently not installed. It is provided by  
several packages. You can install it by typing one of the following:  
  nix-env -iA nixos.ruby
  nix-env -iA nixos.ruby_1_9
  nix-env -iA nixos.ruby_2_0
  nix-env -iA nixos.ruby_2_1
  nix-env -iA nixos.ruby_2_2

However once inside a project I can create a subshell with the environment setup correctly by running nix-shell:

[~/code/mortgages] $ nix-shell default.nix 

[~/code/mortgages] $ ruby --version
ruby 2.2.3p172 (2015-08-18) [x86_64-linux]  

If you're interested in trying Nix out for yourself you can install it on any operating system, Linux is clearly where that will be easiest but I've been told it will still work on OSX and even Windows with Cygwin, but you'll probably have more issues. You can install Nix along side any other package manager and you can easily delete it if it's not for you. You can also go all in with the entire operating system NixOS.