Posted on February 1, 2016
Tags: nerd, docker, nix

One of the cool things about Nix is that it can make Docker images in a declarative and deterministic way. The most excellent @lethalman implemented this in NixOS PR #11156. The reason it works is that Nix can copy the minimal closure of a derivation out of the Nix store.

This is a much faster and more sane way of producing docker images, because:

  1. The intermediate results can be fetched, built, and cached, regardless of build order.
  2. No mucking around with “layers”, union filesystems, etc.
  3. The version of packages you are building is precisely determined by your Nix config, not whatever happens to be the latest result of apt-get upgrade.

Here are some random examples I have prepared. To build them all, run:

nix-build test-docker.nix | xargs -n1 docker load -i

The examples:

# test-docker.nix
{ pkgs ? import <nixpkgs> {} }:

rec {
  # 1. basic example
  bash = pkgs.dockerTools.buildImage {
    name = "bash";
    contents = pkgs.bashInteractive;
  };

  # 2. service example, layered on another image
  redis = pkgs.dockerTools.buildImage {
    name = "redis";
    tag = "latest";

    # for example's sake, we can layer redis on top of bash or debian
    fromImage = bash;
    # fromImage = debian;

    contents = pkgs.redis;
    runAsRoot = ''
      mkdir -p /data
    '';

    config = {
      Cmd = [ "/bin/redis-server" ];
      WorkingDir = "/data";
      Volumes = {
        "/data" = {};
      };
    };
  };

  # 3. another service example
  nginx = let
    nginxPort = "80";
    nginxConf = pkgs.writeText "nginx.conf" ''
      user nginx nginx;
      daemon off;
      error_log /dev/stdout info;
      pid /dev/null;
      events {}
      http {
        access_log /dev/stdout;
        server {
          listen ${nginxPort};
          index index.html;
          location / {
            root ${nginxWebRoot};
          }
        }
      }
    '';
    nginxWebRoot = pkgs.writeTextDir "index.html" ''
      <html><body><h1>Hello from NGINX</h1></body></html>
    '';
  in
  pkgs.dockerTools.buildImage {
    name = "nginx-container";
    contents = pkgs.nginx;

    runAsRoot = ''
      #!${pkgs.stdenv.shell}
      ${pkgs.dockerTools.shadowSetup}
      groupadd --system nginx
      useradd --system --gid nginx nginx
    '';

    config = {
      Cmd = [ "nginx" "-c" nginxConf ];
      ExposedPorts = {
        "${nginxPort}/tcp" = {};
      };
    };
  };

  # 4. example of multiple contents, emacs and vi happily coexisting
  editors = pkgs.dockerTools.buildImage {
    name = "editors";
    contents = [
      pkgs.coreutils
      pkgs.bash
      pkgs.emacs
      pkgs.vim
      pkgs.nano
    ];
  };
}

There are two slight niggles with this function, which I hope can be improved somehow.

Firstly, because the build is deterministic and reproducable, all timestamps must be set to a constant value. Nix chooses the UNIX epoch + 1 second (Jan 1 1970). Therefore, the Docker image is reported to be created 46 years ago! Kind of ironic that the build system of the future has timestamps so far in the past.

Secondly, while the nix closure is minimal in theory, many unnecessary dependencies seem to get dragged into the docker image, bloating its size. For example, the Linux header files.

Update October 2016: My examples got helpfully merged into Nixpkgs as examples.nix. In order to use an image, build its derivation with nix-build, and then load the result with docker load. For example:

$ nix-build '<nixpkgs>' -A dockerTools.examples.redis
$ docker load < result