Generating and running a Rails app with PostgreSQL using Nix on Ubuntu
Isolating a Nix environment by running on an Ubuntu VM provisioned by Vagrant
In my first attempt at setting up a simple Ruby development environment, I was quite careful to make sure the Nix shell environment wasn't accidentally relying on anything available from the underlying environment. In particular, I noticed that unless I specifically added nodejs
to the list of buildInputs
, middleman ended up using the Node version in the underlying MacOS environment.
However, in my second attempt where I set up a simple Rails development environment, I wasn't so careful and I didn't do the same checks on the run-time dependencies.
Ubuntu VM on Vagrant
Previously, in order to ensure full isolation, I ended up editing my "dot" files and even modifying environment variables in the current shell, but this was fiddly and error prone. So this time I decided to setup a completely seperate VM running Ubuntu Xenial (with minimal OS packages) using Vagrant to continue with my Nix experiments.
Furthermore I decided to try to write a provisioning script for the Vagrant configuration to create a new Rails app from scratch and to complete all the steps necessary for getting both the Rails tests and the Rails server running. By doing it this way, I could easily snapshot the VM and restore the snapshot or even destroy the VM and build it again to get back to a clean slate. I found this a really productive way to tackle the problem.
Installing Nix
First I had to install Nix on the Ubuntu VM. I used the Nix multi-user installation instructions to write an inline provisioning script in the Vagrantfile
and I even worked out how to make the script idempotent:
nix --version
if [ $? -eq 0 ]; then
echo 'nix is already installed (skipping installation)'
else
sh <(curl -L https://nixos.org/nix/install) --daemon
fi
Generating a new Rails app
Next I created a nix-shell configuration that made the Rails gem specified by the "outer" Gemfile
available:
nix-shell -p ruby_2_6 bundler bundix --run 'bundle lock && bundix --init --ruby=ruby_2_6'
Much like in my previous article, this generated the following files in the "outer" directory:
Gemfile.lock
gemset.nix
shell.nix
Now that this nix-shell specified by shell.nix
made the Rails gem available, I used it to generate a new Rails app in a subdirectory with the rails new
command:
nix-shell --run 'rails new my-rails-app --skip-bundle --skip-webpack-install --database=postgresql'
This step created a vanilla Rails app in the my-rails-app
subdirectory. Note that I chose to skip the bundle install
and rails webpacker:install
steps, because at this point the nix-shell could not provide everything necessary.
Bundled gems
I felt as if generating the Rails app was separate from setting up and running it and so I decided to create a separate nix-shell within the "inner" subdirectory. This was achieved much as before, but this time based on the "inner" Gemfile
generated as part of the new Rails app:
cd my-rails-app
nix-shell -p ruby_2_6 bundler bundix --run 'bundle lock && bundix --init --ruby=ruby_2_6'
This generated the following files in the subdirectory:
Gemfile.lock
gemset.nix
shell.nix
Runtime dependencies
While the shell.nix
file in combination with the gemset.nix
would make the bundled gems and their dependent OS packages available within the nix-shell, I also needed to add some runtime dependencies (nodejs
, yarn
& ruby_2_6
) for subsequent steps by adding them to the list of buildInputs
in shell.nix
:
buildInputs = [ env nodejs yarn ruby_2_6 ];
PostgreSQL package
Having specified PostgreSQL as the database when I generated the Rails app, I wanted to setup and start a PostgreSQL server in the nix-shell development environment. So I also added the postgresql package to the list of buildInputs
:
buildInputs = [ env nodejs yarn postgresql ruby_2_6 ];
Webpacker
The "inner" nix-shell was now ready to install webpacker using the rails webpacker:install
command.
nix-shell> rails webpacker:install
Running the "inner" nix-shell for the first time resulted in the bundled gems and their OS dependencies being installed along with runtime dependencies mentioned above. I found it pretty cool seeing that e.g. the libxml2
OS package was automatically installed just because nokogiri
was in the bundled gems!
PostgreSQL server
I wanted a PostgreSQL database server to be available, but only from within the nix-shell, i.e. not system-wide. To this end I added a shellHook
to shell.nix
to idempotently configure the server and start it when entering the nix-shell:
shellHook = ''
export PGHOST=$HOME/postgres
export PGDATA=$PGHOST/data
export PGDATABASE=postgres
export PGLOG=$PGHOST/postgres.log
mkdir -p $PGHOST
if [ ! -d $PGDATA ]; then
initdb --auth=trust --no-locale --encoding=UTF8
fi
if ! pg_ctl status
then
pg_ctl start -l $PGLOG -o "--unix_socket_directories='$PGHOST'"
fi
'';
I set the database server up with minimal security to make it easy to access from psql
and so the generated database.yml
would just work out of the box.
Rails development environment
By this stage, the nix-shell Rails development environment was pretty much ready to go. To make things slightly more interesting, I decided to create the canonical simple Rails "blog" app using the rails generate scaffold
command:
nix-shell> rails generate scaffold post title:string content:text
I then created and migrated the development and test databases for the Rails app in the usual way:
nix-shell> rails db:create
nix-shell> rails db:migrate
And ran the Rails tests as follows:
nix-shell> rails test
And finally, the moment of truth (!), I ran the Rails server and opened a browser at the home page π:
nix-shell> rails server --binding 0.0.0.0 --daemon
nix-shell> open http://localhost:3000/
The blogging functionality was also accessible at the relevant endpoint and seemed to persist new posts successfully.
Trying it yourself
If you want to follow along at home, I've published the source code in a GitHub repository. The steps I've described above are documented in the README and the corresponding code is in a couple of inline provisioning scripts within the Vagrantfile
.
Observations
-
Very belatedly, I came across the
--pure
option for nix-shell which might've been a simpler way to isolate my development environment. However, the documentation says: "Note that~/.bashrc
and (depending on your Bash installation)/etc/bashrc
are still sourced, so any variables set there will affect the interactive shell." The latter was the main source of my previous isolation woes, so perhaps it wouldn't have helped that much after all. -
Initially I wanted to have the database-related directories within the Rails app project directory, but it turned out I couldn't do this because VirtualBox doesn't allow hard links within shared directories (at least not on MacOS). And so I put them in the user's home directory instead. If I was doing this "for real", I wouldn't be on a VirtualBox VM and so putting them in the Rails app project directory wouldn't be an issue.
-
I decided not to worry about shutting down the database server when exiting the nix-shell, but I believe this would be fairly straightforward using the Linux
trap
command. -
I discovered that in Nix Ruby packages the Rails gem is fixed at v4.2.11.1. I didn't want to use such an old version and so I ended up using Bundler and Bundix in conjunction with a
Gemfile
to make a newer version of Rails available. If the version in Nix Ruby packages was more up-to-date, I believe I could have used something much simpler, e.g.
nix-shell -p 'ruby_2_6.gems.rails' --run 'rails new my-rails-app --skip-bundle --skip-webpack-install --database=postgresql'
Next steps
-
Multiple Rails app with different database types and versions. This is really the core issue I'm trying to solve by setting my development environment up using Nix.
-
Investigate using Nix to somehow make Node packages available to the environment in a similar way to Bundix instead of using Yarn directly, i.e. also automatically installing any OS package dependencies.
-
Investigate using Nix home-manager to provide a more generic environment on the VM to create the Rails app, i.e. to be able to run
rails new
.
Further reading
-
Someone on the Nix forums pointed me at this Ruby guide for Nix which for some reason hasn't been incorporated into the main Nix documentation.
-
At various points I've found it useful to dive into the source code of the Nix Ruby modules. This was particularly useful when I was trying to understand how Bundix worked.