I love Nix. I’ve been running NixOS for the past few weeks and it’s been a great experience.

However, I started a new job recently and have been thinking about our deployment pipeline and how we will manage build and run-time environments. Finding the boundary between Nix and Docker in this area has been consuming me for the past few weeks.

I want to maximize the human element. I want one tool to declaratively (and deterministically) control our build- and run-time dependencies, while also gaining the portability (and industry traction) of Docker containers. I want to on-board new engineers by saying:

# want to work on this project? # Step one: install nix git clone $PROJECT cd $PROJECT nix-shell

and now they’re in a shell with every run- or build-time dependency installed. But not all engineers will work on every service. This is where Docker shines. I also want to say:

# need to run $SERVICE? # Step one: install docker sudo docker run -t $SERVICE -p 8080:8080

How do we combine these two, seemingly disparate tools? Is there a proper intersection? Nix excels at managing per-project dependencies and works across distros (and even on OSX and Cygwin (modulo some in-progress work being done)), so it should work within any Docker container!

Is it possible to run Nix within Docker?

/rant

Suppose you have the following minimal default.nix that specifies a build environment with the Haskell compiler GHC as a dependency:

# default.nix { nixpkgs ? (import <nixpkgs> {}) }: let stdenv = nixpkgs.stdenv; ghc = nixpkgs.haskellPackages.ghc; in stdenv.mkDerivation rec { name = "our-project"; version = "0.0.1"; src = fetchurl { url = "http://your.domain/run.tar.gz" }; buildInputs = [ ghc ]; }

And suppose you want to create a Docker container that will run an executable ( run ) in a shell environment as defined by default.nix :

# Dockerfile FROM debian:wheezy # Install packages required to add users and install Nix RUN apt-get update && apt-get install -y curl bzip2 adduser # Add the user aaronlevin for security reasons and for Nix RUN adduser --disabled-password --gecos '' aaronlevin # Nix requires ownership of /nix. RUN mkdir -m 0755 /nix && chown aaronlevin /nix # Change docker user to aaronlevin USER aaronlevin # Set some environment variables for Docker and Nix ENV USER aaronlevin # Change our working directory to $HOME WORKDIR /home/aaronlevin # install Nix RUN curl https://nixos.org/nix/install | sh # update the nix channels # Note: nix.sh sets some environment variables. Unfortunately in Docker # environment variables don't persist across `RUN` commands # without using Docker's own `ENV` command, so we need to prefix # our nix commands with `. .nix-profile/etc/profile.d/nix.sh` to ensure # nix manages our $PATH appropriately. RUN . .nix-profile/etc/profile.d/nix.sh && nix-channel --update # Copy our nix expression into the container COPY default.nix /home/aaronlevin/ # run nix-build to pull the RUN . .nix-profile/etc/profile.d/nix.sh && nix-build # run our application CMD ["./results/bin/run"]

This Dockerfile will:

install nix

add a non-sudo-privileged user aaronlevin (yay security)

(yay security) copy the default.nix nix expression to a working directory

nix expression to a working directory build the default.nix nix expression, installing all its dependencies ( ghc ) and unpacking the run executable in the process.

nix expression, installing all its dependencies ( ) and unpacking the executable in the process. run your application.

With these two in a working directory you can run:

sudo docker build -t myproject . sudo docker run -t myproject

Why?

By placing all your run- and build-time dependencies in nix expressions you have a single, common language that can be used locally on your dev machines or within docker containers. You get the consistency of deterministic builds across dev machines and docker containers!

nix-docker?