Build your nodejs project with nix in 10 minutes or less
!!! Warning: dream2nix is unstable software. While simple UX is one of our main focus points, the APIs are still under development. Do expect changes that will break your setup.
This guide takes you step-by-step through setting up a nodejs reproducible build and development environment using nix (the build system) and dream2nix (bridge between external package managers and nix).
This setup will allow for the same environment to be reproduced on different machines and CI systems with high accuracy - thus avoiding many pitfalls of distributed software development.
Outline
- Install nix with flakes enabled
- Navigate to your nodejs project
- Initialize the dream2nix flake
- Define target platform(s)
- Explore the outputs
- Build the project
- Development shell
- FAQ
Install nix
If you don't have nix already, check out nixos.org/download.html on how to install it.
Enable the nix flakes feature
For internal dependency management dream2nix requires the experimental nix feature flakes
being enabled.
export NIX_CONFIG="extras-experimental-features = flakes nix-command"
If you find yourself using dream2nix regularly, you can permanently save these settings by adding the following line to your /etc/nix/nix.conf
:
experimental-features = flakes nix-command
Navigate to your nodejs project
For this guide we will use the fun
cowsay
nodejs project.
It is simply a talking cow for your console. This project is a nodejs
port from the original perl version.
Feel free to use any other project, if you do and hit a roadblock,
please consult the FAQ at the end of this article for solutions
to some common issues.
We start by cloning the project:
> git clone https://github.com/piuccio/cowsay /tmp/my_project
> cd /tmp/my_project
Initialize the dream2nix flake
We have our repository cloned and ready. Now we will create the flake.
The flake is a standalone description of the project, it will define the inputs and outputs of our project, the build steps - in this case handled by dream2nix, and the development environment. The flake is a fully standalone and complete configuration for nix to build a software package.
We use a dream2nix flake template:
> nix flake init -t github:nix-community/dream2nix#simple
wrote: /tmp/my_project/flake.nix
to create a flake.nix
:
{
inputs.dream2nix.url = "github:nix-community/dream2nix";
outputs = inp:
inp.dream2nix.lib.makeFlakeOutputs {
systemsFromFile = ./nix_systems;
config.projectRoot = ./.;
source = ./.;
projects = ./projects.toml;
};
}
This file configures our build and development environment using the
dream2nix framework to bridge the nodejs ecosystem into nix. This let's
nix read and understand package.json
and how to install and link
nodejs packages - to avoid duplication of dependency definitions and
build steps.
Define target platform(s)
We have the flake setup, now we need to define the supported systems, this is necessary because nix can do multi platform and cross-platform builds so we need to tell it what can be built and where.
There are 2 ways to do this, either with a nix_systems
file,
or we can write the target platforms inline to our flake.nix
.
nix_systems
We can create a nix_systems
file with the current system:
> nix eval --impure --raw --expr 'builtins.currentSystem' > ./nix_systems
> git add ./nix_systems
The nix_systems
file is simply a list of the supported systems, for example:
x86_64-linux
Remember to add the file ./nix_systems
to git, or it won't be picked up by nix.
If you want to support more platforms later, just add more lines to that file.
inline
Alternatively, we can define the targets in the flake.nix
like so:
{
inputs.dream2nix.url = "github:nix-community/dream2nix";
outputs = inp:
inp.dream2nix.lib.makeFlakeOutputs {
systems = ["x86_64-linux"]; # <- This line.
config.projectRoot = ./.;
source = ./.;
projects = ./projects.toml;
};
}
This has the advantage of keeping all the configuration in a single file.
Populating projects.toml
dream2nix also needs to know things about the project(s) at hand.
In the flake.nix
file you can see it's expecting a ./projects.toml
.
The easiest way to create and populate this ./projects.toml
is with the helper function
nix run github:nix-community/dream2nix#detect-projects . > projects.toml
git add projects.toml
Explore the outputs
We have setup the flake, defined our target system(s), now we are ready to use it. Let's start by listing out what is available to us (actual output may be different, this is a shortened version):
> nix flake show
warning: Git tree '/tmp/my_project' is dirty
warning: creating lock file '/tmp/my_project/flake.lock'
warning: Git tree '/tmp/my_project' is dirty
git+file:///tmp/my_project
├───devShell
│ └───x86_64-linux: development environment 'nix-shell'
├───devShells
│ └───x86_64-linux
│ ├───cowsay: development environment 'nix-shell'
│ └───default: development environment 'nix-shell'
└───packages
└───x86_64-linux
├───cowsay: package 'cowsay-1.5.0'
├───default: package 'cowsay-1.5.0'
└───resolveImpure: package 'resolve'
We can see that:
warning: Git tree '/tmp/my_project' is dirty
Our repository has uncommitted changes. Nix uses git commit hashes to version build artifacts, so this can result in some extra rebuilds. Since we are just setting up the project now, this is alright.warning: creating lock file '/tmp/my_project/flake.lock'
Our flake itself has an input (external dependency), thedream2nix
framework. When we first use the flake, like we just did, nix created a lock file with the exact version of the input (and its inputs). Commit this file to version control to ensure reproducible builds.- Finally, we see the outputs of our flake.
We see it outputs
packages
for thex86_64-linux
systems: thecowsay
package (our nodejs project) and aresolveImpure
package (more about that in the next section). It also sets outcowsay
package as thedefault
package of this flake.
Build the project
We have setup our flake for the nodejs project and identified the output we want to build.
To build the output, we run:
> nix build .#cowsay
(The .
means the flake in the current directory, #
is a special
character separating the flake name and the package name, and cowsay
is
the name of the package we want to build. Since we want to build
, nix
will look under packages
first, and it knows our current platform
(x86_64-linux
in this case), so it will actually build the output
.#packages.x86_64-linux.cowsay
.)
Since, cowsay
is the default
package, we could also simply run:
> nix build
To build the .#packages.x86_64-linux.default
output. (In our case
these are the same.)
This creates our ./result
directory with all our final build
artifacts.
> ./result/bin/cowsay 'hello dream2nix'
_________________
< hello dream2nix >
-----------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Nix was able to build this project, because it has a package-lock.json
file pinning the exact dependency versions. If we did not have this
file, the nix build
would fail with error: unresolved impurities
.
> git rm package-lock.json
> nix build .#cowsay
warning: Git tree '/tmp/my_project' is dirty
error: The nodejs package cowsay contains unresolved impurities.
Resolve by running the .resolve attribute of this derivation
or by resolving all impure projects by running the `resolveImpure` package
We can fix this by generating a language specific lockfile
(package-lock.json
or yarn.lock
for nodejs), or let dream2nix
generate a universal dream-lock.json
.
> nix run .#resolveImpure
warning: Git tree '/tmp/my_project' is dirty
Resolving:: Name: cowsay; Subsystem: nodejs; relPath:
translating in temp dir: /tmp/tmp.SwBFt0WcH4
up to date, audited 172 packages in 9s
57 packages are looking for funding
run `npm fund` for details
3 high severity vulnerabilities
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
===[SUCCESS]===(dream2nix-packages/cowsay/dream-lock.json)===
adding file to git: dream2nix-packages/cowsay/dream-lock.json
This runs npm
behind the scenes and resolves dependencies for all
packages inside the flake.
There is no difference between using an external lockfile or
dream-lock.json
, all they do is pin dependency version and are
completely interchangeable.
Development shell
We were able to build our nodejs project with nix, however our build
artifacts under ./result
are read-only and we do not have node
and
npm
in PATH
. To be able to work in this project we will need those.
Nix provides us with devShells
for exactly this.
> nix flake show
warning: Git tree '/tmp/my_project' is dirty
warning: creating lock file '/tmp/my_project/flake.lock'
warning: Git tree '/tmp/my_project' is dirty
git+file:///tmp/my_project
├───devShell
│ └───x86_64-linux: development environment 'nix-shell'
├───devShells
│ └───x86_64-linux
│ ├───cowsay: development environment 'nix-shell'
│ └───default: development environment 'nix-shell'
└───packages
└───x86_64-linux
├───cowsay: package 'cowsay-1.5.0'
├───default: package 'cowsay-1.5.0'
└───resolveImpure: package 'resolve'
When we enter the cowsay
development shell, we will get node
in our
PATH
, together with all the binaries from our dependencies packages.
And nix will copy over node_modules
for us to save us from having to
npm install
everything over again.
To get in the shell, simply run:
> nix develop -c $SHELL
(The -c $SHELL
part is only necessary if you use a different shell than bash
and would like to bring that shell with you into the dev environment.)
From here on it's the same as using a normal installation of nodejs.
However, if we do imperative changes to node_modules
and later
re-enter the nix development shell, nix will overwrite the
node_modules
with the pinned versions of the dependencies from our
lockfile.
FAQ
Refusing to overwrite existing file on flake init
.
When initializing a flake it needs to write some files, if these already
exists, the initialization will fail. Since our repository is under
version control, we can delete the conflicting files, let flake init
create them and then check the diff and merge the changes manually.
Getting status of flake.nix
: no such file or directory.
The flake build does not happen inside the directory. Nix copies your
repository to a temporary location and builds there; only files under
version control are used. To resolve this run git add flake.nix
and
all other missing files.
Warning: Git tree is dirty
This is just a warning, nix is using the git revision for build artifact versioning. Having a dirty git tree - meaning uncommitted changes - can lead to some extra rebuilds, for simple projects this should not be a major concern.
error: The package contains unresolved impurities. Resolve all impure projects by running the resolveImpure
package.
This happens when dream2nix cannot resolve exact package versions. We
can define a dependency like something@^2.1
, but it is not obvious if
we actually want 2.1.1
or 2.1.2
or maybe 2.1.1-alpha
.
There are 2 ways to resolve this error: either by running
nix run .#resolveImpure
and letting dream2nix resolve the most
up-to-date versions of all dependencies, or using an external package
manager to generate a lock file, which can be later read by dream2nix.
(In case of Node.js, both package-lock.json
and yarn.lock
are
supported.)