A predictable, stable environment (in terms of your operating system, system libraries, build tools, and programming libraries) is essential to each development step: from onboarding, to collaboration, continuous integration, quality assurance, and deployment. Deviation can cause one-off, intermittent, and even catastrophic failures.
However, consistency can be elusive, even with the best intentions, best practices, and tools in place, because:
- Developer machines are easily polluted with clashing libraries and binaries.
- Programming libraries — Ruby's gems or Python's eggs, for example — are regularly updated on unadvertised and unpredictable schedules.
- Docker images can be removed from public repositories without notice.
- Continuous integration providers change internals without forewarning or ex post facto notice.
Nix aims to solve some of these issues. Specifically, it is designed to reproduce packages and environments verbatim. Given a set of inputs, it generates the same output every time.
Let's explore how Nix can reproduce an environment for Ruby and Rails development, including a version of Ruby, a collection of gems, a PostgreSQL database, and a Redis cache.
An Overview of Nix
Nix is an entire universe of software. It runs on Linux and macOS, on both Apple Silicon and Intel processors, and has several components:
- The Nix Programming Language is a declarative, domain-specific, functional language dedicated to system composition. Portions of the language manipulate Nix directly, while others provide typical programming constructs like flow control and variables.
- Nix Packages is an expansive repository
of software, from
awk
tozsh
. At the time of writing, the repository contains more than 100,000 entries, each usable with Nix. Each package in the Nix archive explicitly documents all its dependencies down to a specific commit. Further, the nixpkgs archive is itself versioned, providing even greater control over package provenance. nix-shell
provides a virtual, sacrosanct environment contained in a shell. Given a Nix description of a system — a derivation in Nix parlance — you can compose and boot a system in a walled garden isolated from all other virtual and physical systems. For example, you can download, install, build, and run an independent instance of Ruby withnix-shell
.- The Nix Store persists immutable data (such as software packages) and all dependencies. On some systems, the Nix Store is the local file system; however, it can also be retained on Amazon S3 and even a remote SSH server.
- NixOS is an entire Linux distribution configured entirely by the Nix language and composed from versions in Nixpkgs.
Nix is expansive and each component warrants deep exploration. This
introduction focuses on the nix-shell
and Nix Packages and presents
just enough of the Nix programming language to boot a Rails application.
Tony Finn's Nix from First Principles
provides another take on Nix.
Nix Versus Docker
Docker and Nix are both capable of building environments. However, Nix provides consistency everywhere, including build tools and source packages.
Docker is not a package manager. While you can connect and coordinate containers using Docker Compose, you cannot use Docker to combine containers into a new container.
It's trivial with Nix to combine disparate Ruby and PostgreSQL derivations into a new environment.
If a Docker image changes without notice, the output of docker build
changes too. Pinning a version obviously helps, but if a
Docker image is deleted or an image's repository goes offline, a
Docker build fails without recourse.
Each package and version stored by Nix can have a unique ID akin to a git SHA. You can think of a complete Nix derivation as a manifest of tens or hundreds of SHAs. A change in any one SHA is detectable and two derivations will not be the same if a single entity has changed.
Get Started with Nix
Let's dive into Nix.
To install Nix natively on Apple MacOS, open the Terminal application in Applications/Utilities and run the following command at the shell prompt:
Enter the password for the root
user of your system when prompted.
(If you are a Mac administrator, you can enter your password too.)
You need privileged access as the install creates a new storage volume specifically
for Nix and tweaks several system settings, such as excluding the Nix store
from Time Machine backups.
The installer summarizes all the modifications it plans to perform and
prompts you to proceed. Enter Yes
.
The installation takes only a moment or two to finish.
You can easily test it when it's done by running the following sequence of commands in your current shell.
The nix run
command is something akin to magic. To run the hello
utility, Nix:
- Downloads all the packages required to build
hello
, including tools such asmake
,gcc
, andtar
. (On the machine used to develop this article, an i9 iMac running MacOS Sonoma, Nix downloaded more than 400 prerequisite packages into /nix/store.) - Downloads the most recent version of
hello
(as no version number was specified). - Builds and executes the utility, producing
Hello, World!
. - Terminates and returns to the original shell.
If you run the same command — nix run 'nixpkgs#hello'
— again, Nix executes hello
immediately, as the assets required for the utility are cached and readily accessible.
You can use Nix to run almost any utility on-demand. Here is another example.
The penultimate line of the output above is the default fish
prompt, showing a user
name, machine name, an abbreviated current working directory name, and the current
git branch, if any. Press Control-D to exit the shell.
If you're following along, try this: Launch fish
via Nix and enter the command
which hello
. The result should look like this:
which hello
fails in the current environment. ([1]
is the return value
of the previous command, not the output of the previous command. Shell
utilities return non-zero values on failure.) Where is the hello
from the
previous install? It's in its own encapsulated derivation, completely isolated
from the one created for fish
.
A one-off Nix derivation such as the one above has its uses, but a Nix derivation is not limited to a sole utility. A derivation may contain any number of packages. Better yet, a derivation can be declared, shared, and re-instantiated in identical form on any machine.
Nix for Ruby Development
Let's create a Nix derivation for Rails development. The derivation must have:
- A version of Ruby, such as 3.2.0 (the latest version of Ruby available
via Nix at the time of writing). Each Ruby package includes
gem
andirb
. - A database service to persist information. Here, let's use PostgreSQL Version 15.
- A key store for caching data, partials, and views. Redis is quite capable.
- Tools for JavaScript and asset compilation.
Of course, a plenary Rails environment also needs the rails
gem and many
others. Ideally, a Nix derivation for Rails development also automatically
bundles necessary gems, producing a standalone environment ready for
development.
A Nix Derivation for Ruby Development
The code below is a nascent, but working, Nix derivation for Rails development. A Nix derivation is like a Dockerfile or a blueprint to build a working environment from scratch.
The sample derivation has two sections. The let
section defines settings,
while the in
portion controls the build and enumerates the packages
to construct and install in the derivation.
Before discussing how to use the derivation, some commentary:
- The setting
nixpkgs
requires a specific version of the Nix archive. Like agit
repository, each iteration of the Nix packages archive is represented by a unique SHA reflecting the state of its contents. If the contents of the archive change, such as by incorporating a new version of Ruby, the SHA changes in tandem. Similar to referencing specific commits ingit
, you can lock your derivation to a specific iteration of the archive. The particular archive referred to here,ee4a6e0
, contained a bug fix required to build the example derivation. - The setting
targetRuby
specifies a Ruby version. Here, the derivation uses the latest version of Ruby 3.2 available in the archive. - The setting
myBundler
customizes the Nix build to use the same version of Ruby specified for the shell environment. - Lastly, the setting named
gems
configures the Nix version ofbundler
. As with nativebundler
, the Nix bundler requires a Gemfile and a Gemfile.lock. gemset.nix is original to Nix and is a translation of Gemfile.lock to the Nix syntax.
If you want to know what versions of Ruby are available, point your browser
to the Nix Packages Index and search for
ruby
. The search results include all the Ruby iterations available, such as
ruby
, ruby_3_2
, and ruby_3_3
, for Ruby 3.1, 3.2, and 3.3, respectively.
Each package in the search result specifies the exact version number, such as
3.1.4
for the ruby
package.
If you want to use a specific version of Ruby that is not named in the archive, try
nixpkgs-ruby. For instance, to use Ruby 3.2.2, replace the line
targetRuby = nixpkgs.ruby_3_2;
in shell.nix with this code:
Running the Nix Shell
Using the Nix derivation for Rails development proceeds much like development in any shell. The big exception: There is no need to install Ruby or attendant services beforehand. If you have Nix installed, you're ready to go.
-
Install Nix if necessary.
-
Clone a Rails project into a new directory or choose an existing codebase.
cd
to the directory for the project. -
Copy and paste the code for the sample derivation to a new file in the project directory named shell.nix. (shell.nix is the customary name for a derivation, but the name is otherwise arbitrary.)
-
Generate gemset.nix to catalog the gems used within the project.
This command runs
bundix
, a Nix program, to convert Gemfile.lock into gemset.nix. Both are gem manifests, albeit the latter expressed natively in the Nix language. For comparison, here is an entry foractioncable
from gemset.nix and from Gemfile.lock. -
Generate and launch the derivation.
The --verbose
option is not required, but provides good and numerous insights
into how nix-shell
assembles the environment.
The Downsides: Nix has Some Flaws
Nix is a rich, robust, and burgeoning option for building, reproducing, and sharing environments. Nix is also an emergent tool: its current users are early adopters.
Nix is already a workable solution for many problems, but Ruby support seems to be a work in progress. For example, at the time of writing, no official archive is available for newer versions of Ruby 3.3, and there is no guarantee the latest Nix archive contains every gem in your Gemfile. Documentation for Ruby on Nix is scattered between various GitHub repos, the official Nix website, and user forums. I am grateful to a number of intrepid Nix developers who helped me craft the sample shell.nix, including Evan Travers, Bob van der Linden, and Norbert Melzer.
Nix currently is akin to
git's "porcelain":
powerful but esoteric. However, much like git
evolved into exoteric,
user-friendly tools such as
git-flow,
GitHub Desktop, and
Tower to become user-friendly, many developers are
building abstractions, wrappers, and utilities to simplify Nix usage. Let's briefly look at a few of these tools now.
Tools to Help with Nix: devbox
, devenv.sh
, and fleek
A tool to experiment with is devbox
. devbox
turned a 10-year-old Macbook Pro laptop running macOS Big Sur into a development
machine for a modern Rails application in a few minutes. The setup did not
include automatic bundling of gems, but it did install Ruby (including bundler
and gem
), Redis, and PostgreSQL in an isolated and pristine shell environment.
devbox
eschews a bespoke programming language for configuration and uses JSON.
The environment on the MacBook Pro booted from this minimal file named
devbox.json:
devenv.sh
merits exploration too. It is something of a
hybrid, with a JSON-like programming language, YAML configuration, and
Docker-like composition of services.
fleek
is another
Nix-based tool to craft and share configurations for development machines. It
also avoids the Nix programming language and instead provides Homebrew-like
commands to build an environment.
Wrapping Up
In this post, we've seen how Nix can help reproduce a stable environment for Ruby and Rails applications.
While it has some flaws, Nix and the myriad offshoots in development are worth watching.
Go forth and hack!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!