Nix and direnv on Ubuntu server

Reproducible development environments for all

Quickly get up and running with flake-supported development environments on an arbitrary Linux system. Done on Ubuntu Server 18.

Preparations

Nix needs the nix store to reside in /nix. This folder cannot be a symlink, however, bind-mounts can be used to mount it from elsewhere if your root is too small. Depending on the usage, this folder can grow significantly over time, so I’d recommend at least 100 GB to get along for a while. The more you do with Nix, the bigger it will become.

TODO: Instructions for mounting

Installing nix

$ wget https://nixos.org/nix/install -O nix-install
$ sh nix-install --daemon

Follow the install instructions. Once done, verify the installation:

$ nix-shell -p nix-info --run "nix-info -m"
[... lots of stuff ...]
 - system: `"x86_64-linux"`
 - host os: `Linux 4.15.0-166-generic, Ubuntu, 18.04.6 LTS (Bionic Beaver)`
 - multi-user?: `yes`
 - sandbox: `yes`
 - version: `nix-env (Nix) 2.5.1`
 - channels(root): `"nixpkgs-22.05pre344305.32356ce11b8"`
 - nixpkgs: `/nix/var/nix/profiles/per-user/root/channels/nixpkgs`

Configuring nix

We intend to use Nix Flakes as they provide features well suited for managing development environments that come with a project. They are still officially experimental, but already widely used. Edit /etc/nix/nix.conf:

keep-outputs = true
keep-derivations = true
experimental-features = nix-command flakes

trusted-users = root

build-users-group = nixbld

You should now be able to use flakes. To verify:

$ mkdir test
$ cd test
$ nix flake init
$ nix build .
$ ./result/bin/hello

Hello, world!

Installing nix-direnv

This is useful for automatically activating a build environment once the project folder (or a subfolder) is entered. Install direnv. We have to do this natively because Nix isn’t managing our global environment.

$ sudo apt install direnv

To enable direnv for all users, we can create a file /etc/profile.d/direnv.sh containing the following:

if [ -n "$ZSH_VERSION" ]; then
    eval "$(direnv hook zsh)"
elif [ -n "$BASH_VERSION" ]; then
    eval "$(direnv hook bash)"
fi

A new login shell should now detect folders containing a .envrc. Verify:

$ cd test
$ touch .envrc

direnv: error .envrc is blocked. Run `direnv allow` to approve its content.

Now clone nix-direnv to, e.g., /opt as follows:

$ cd /opt
$ git clone https://github.com/nix-community/nix-direnv

Activate nix-direnv

There does not seem to be a system-global .direnvrc so the following needs to be done for each user. Create $HOME/.direnvrc containing:

source /opt/nix-direnv/direnvrc

You should now be able to automatically build and enter the development environment containing a flake. To verify:

$ cd test
$ echo "use flake" > .envrc

direnv: error .envrc is blocked. Run `direnv allow` to approve its content.

$ direnv allow
[... lots of stuff ...]

$ which gcc
/nix/store/npm4g1zsj5yzygf6bq46pbi9fqhxisha-gcc-wrapper-10.3.0/bin/gcc

$ cd ..
$ which gcc
# May be empty if gcc is not installed, or show system gcc

A real example

Quickly spin up a Rust environment to build a firmware image for a Raspberry Pi 2040 micro-controller? Never done Rust before? No problem.

$ git clone https://github.com/polygon/rp2040-project-template-nix
$ cd rp2040-project-template-nix

direnv: error .envrc is blocked. Run `direnv allow` to approve its content.

$ direnv allow
[... Be patient, setting up a Rust env is a lot of work. That you don't have to do. ...]
$ cargo build

That was easy.