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?
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:
nixaaronlevin (yay security)default.nix nix expression to a working directorydefault.nix nix expression, installing all its dependencies (ghc) and unpacking the run executable in the process.With these two in a working directory you can run:
sudo docker build -t myproject .
sudo docker run -t myproject
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!
There is an interesting project named nix-docker that creates Docker containers based on NixOS configurations. This is great if you’re comfortable with NixOS being the base of your Docker image and managing everything from a single configuration.nix. I like this idea, but I’m not totally won over yet. Nix really excels at working across distros, and there’s something to the simplicity of having any base image, installing Nix, and running from there.