While Nix is most widely known for its core features, like fully reproducible package builds and hermetic development environments, it has numerous features that are less fundamental to its value proposition but nonetheless extremely useful. Today, I’d like to bring one such Nix feature into focus: Nix’s ability to run executables with zero setup and nothing more than a URL in hand. Here’s an example:
nix run github:DeterminateSystems/riffIf you have Nix installed and flakes enabled,
this one command enables you to run Riff in one go, without
needing to run nix profile install or apt-get or brew install or anything
else. There’s really nothing quite like it in the software world, and I’m
excited to let you in on this little secret.
The core mechanism: Nix flakes
The nix run feature only works with Nix
flakes, that is, with any Nix project with a
properly structured flake.nix
file in the root.. There are two Nix flake
outputs that you can directly
nix run: packages and apps.
Nix packages
While flake package outputs are typically used to build final artifacts, such
as nix build .#my-app, you can also nix run those outputs if **the built
derivation has a bin directory with an executable in it. If so, Nix tries
these executable names in order:
- The
meta.mainProgramattribute of the derivation - The pname attribute of the derivation
- The name attribute of the derivation
If none of those are successful, then you can’t nix run the package output.
The command nix run github:DeterminateSystems/riff works, for example,
because the default package output has
pname set
to riff.
Nix apps
Nix apps are executable programs that you can specify using an attribute set with two fields:
1{2 apps.default = {3 type = "app";4 program = "${myPkg}/bin/my-pkg";5 # Assuming there's a local derivation called myPkg6 };7}If you add the above to your flake outputs, then you can run the app using nix run in the current directory or nix run <flake> if flake.nix is somewhere
else. Because this is the default app, you don’t need to specify an app name
like nix run .#my-app. To specify multiple apps in a flake:
1{2 apps = {3 my-linter = {4 type = "app";5 program = "${myLinter}/bin/my-linter";6 };7
8 my-checker = {9 type = "app";10 program = "${myChecker}/bin/my-checker";11 };12 };13}To run these apps:
nix run <flake>#my-linternix run <flake>#my-checker
# If the flake were in the current directorynix run .#my-linternix run .#my-checker
# If the flake were in a GitHub reponix run github:my-org/my-repo#my-linternix run github:my-org/my-repo#my-checkerUsing Nix apps for nix run instead of packages is especially beneficial when
a single derivation builds multiple executables. Here’s an example:
1{2 apps = {3 # nix run <flake>#server4 server = {5 type = "app";6 program = "${myPkg}/bin/server";7 };8
9 # nix run <flake>#client10 client = {11 type = "app";12 program = "${myPkg}/bin/client";13 };14 };15}Full example
Let’s dig in a bit more concretely. Let’s say that you’re working on a tool
called runme in a public Git repo at https://github.com/AmbiguousTechnologies/runme. Your
flake.nix looks like this:
1{2 description = "runme application";3
4 inputs = {5 nixpkgs.url = "nixpkgs"; # Resolves to github:NixOS/nixpkgs6 # Helpers for system-specific outputs7 flake-utils.url = "github:numtide/flake-utils";8 };9
10 outputs = { self, nixpkgs, flake-utils }:11 # Create system-specific outputs for the standard Nix systems12 # https://github.com/numtide/flake-utils/blob/master/default.nix#L3-L913 flake-utils.lib.eachDefaultSystem (system:14 let15 pkgs = import nixpkgs { inherit system; };16 in17 {18 # A simple executable package19 packages.default = pkgs.writeScriptBin "runme" ''20 echo "I am currently being run!"21 '';22
23 # An app that uses the `runme` package24 apps.default = {25 type = "app";26 program = "${self.packages.${system}.runme}/bin/runme";27 };28 });29}With that configuration in place and pushed to the repo, you could run the runme script with just one command:
nix run github:AmbiguousTechnologies/runmeIn this case, runme is the default app so nothing needs to come after the
flake URL, but let’s say that you add a second app, called runme-lint:
1{2 apps = rec {3 default = runme;4
5 runme = {6 type = "app";7 program = "${self.packages.${system}.runme}/bin/runme";8 };9
10 lint = {11 type = "app";12 program = "${self.packages.${system}.runme-lint}/bin/runme-lint";13 };14 };15}You can now run both of those apps:
nix run github:AmbiguousTechnologies/runmenix run github:AmbiguousTechnologies/runme#runme # Equivalent to the above
nix run github:AmbiguousTechnologies/runme#runme-lintAn important thing to note here is that these nix run invocations
require zero installation or setup. Here’s what happens instead:
- Nix inspects the
programstring in each app and determines that it needs to build a new package. - Nix builds the package and all of its dependencies and stores the
results in the local Nix store (by default under
/nix/store). - Once the package has been built, Nix can run it directly from the
local store. In this case, the
runmepackage may end up in a location likenix/store/khid71caxbq4g8dhfln8rihyclmrz7c3-runme. But the installation process happens seamlessly and without any user input.
This multi-step process accounts for why the first nix run invocation usually
takes a while but is near-instantaneous on future runs. If you opt to use nix run for running packages—instead of installing them using nix profile install or Home Manager or
some other means—you should take this potential time lag into account.
Using Git revisions as a versioning mechanism
For Nix flakes that are Git repositories, you can point nix run at specific
Git revisions to run different versions of a package or app. Here’s the basic
syntax for specific revisions on GitHub, along with some examples:
# Syntax
nix run github:<owner>/<repo>/<revision>#<executable>
# Examples
## Specific commit IDnix run github:DeterminateSystems/riff/a71a8b5ddf680df5db8cc17fa7fddd393ee39ffe
## Tagnix run github:DeterminateSystems/riff/v1.0.0
## Latest commit in a branchnix run github:DeterminateSystems/riff/secret-branch-for-nix-run
## Target a flake in a subdirectorynix run "github:hard-to-find/cool-app?dir=nested#specific-app"Let’s say that you’ve released a great new tool called quicknav at version
0.1.0. You’ve worked out the kinks and it’s stable enough to offer to the
world. But late at night you implement a wacky idea for the UI that’s
definitely not yet ready for a stable release, but you think your friends
would be entertained by it. You want them to try it out. What
to do? With Nix, one possibility is to push those changes to separate branch,
let’s say experimental-ui, and let your friends run that:
nix run github:quicknav/quicknav/experimental-uiIn this case, github:quicknav/quicknav and
github:quicknav/quicknav/experimental-ui aren’t “versions” of one flake—they’re really just
different flakes. In other words, Nix understands both of those addresses as different “universes” of Nix
code with no necessary connection to each other. The same applies to all Git refs.
Security warning
It bears noting here that nix run has a pretty loose trust model.
Running nix run github:DeterminateSystems/riff today and then again tomorrow,
for example, could execute entirely different executables because the flake
address (github:DeterminateSystems/riff) could resolve to different Git
commits at different times. If you’re concerned about this problem—and you
should be!—the best course of action is to always tie your nix run
invocations to specific commits or to Git refs that resolve to a known
commit. Below you’ll find a bad nix run and a good nix run:
# 🚫 BADnix run github:kinda-sketch/could-be-a-bitcoin-miner
# ✅ BETTER (if you've already vetted the code for this commit)nix run github:kinda-sketch/could-be-a-bitcoin-miner/0a8e7a241db7938b10062218309ba30f6d9f0e2dGranted, worrying about Git refs negates much of the “a-ha!” effect of
nix run, but if you plan on using this feature with less-than-100%-trusted remote flakes, it’s
nonetheless advised.
How to find out which nix run targets are available
With the Nix CLI, you can run nix flake show <flake> to see
what a flake outputs. Let’s see which packages are output by the flake in the
Riff repo:
nix flake show github:DeterminateSystems/riffThe output:
github:DeterminateSystems/riff/a54624ac12aa2c125d8891d124519fba17aa2016│├───defaultPackage │││ ├───aarch64-darwin: package 'riff-1.0.2' │││ ├───aarch64-linux: package 'riff-1.0.2' │││ ├───x86_64-darwin: package 'riff-1.0.2' │││ └───x86_64-linux: package 'riff-1.0.2'# Other outputFrom this we can see that Riff’s flake outputs a default package on four
different platforms. When you run nix run, Nix infers the current host
platform (in my case aarch64-darwin) and runs the system-specific package if
it’s available or errors out if it isn’t available.
Find out more about how Determinate Systems is transforming the developer experience around Nix
Implications
To my knowledge, no software ecosystem outside of Nix provides a comparably
baked-in mechanism for running executables. Containerization tools like
Docker and Podman get you
somewhere in the ballpark by enabling you to use docker run and podman run
to target OCI-compliant images in container registries, but both of those tools
require that the image be already built and pushed, whereas Nix needs nothing
more than a properly formed Nix expression exposed through a known flake URL.
While I wouldn’t necessarily call this a primary, bread-and-butter use case for Nix, I do think that it provides a vivid illustration not just of what Nix can do but also of the power of the Nix flake model for distributing expressions. With Nix, you can often get things done without needing to distribute build artifacts. All you need is valid Nix code and the Nix CLI can build—and run—arbitrarily complex artifacts on the current host. And with flakes providing convenient “addresses” for Nix code, you end up with a robust mechanism for distributing executables.