nlewo / nix2container

An archive-less dockerTools.buildImage implementation
Apache License 2.0
552 stars 49 forks source link
containers docker nix nixos oci skopeo

nix2container

nix2container provides an efficient container development workflow with images built by Nix: it doesn't write tarballs to the Nix store and allows to skip already pushed layers (without having to rebuild them).

This is based on ideas developed in this blog post.

Getting started

{
  inputs.nix2container.url = "github:nlewo/nix2container";

  outputs = { self, nixpkgs, nix2container }: let
    pkgs = import nixpkgs { system = "x86_64-linux"; };
    nix2containerPkgs = nix2container.packages.x86_64-linux;
  in {
    packages.x86_64-linux.hello = nix2containerPkgs.nix2container.buildImage {
      name = "hello";
      config = {
        entrypoint = ["${pkgs.hello}/bin/hello"];
      };
    };
  };
}

This image can then be loaded into Docker with

$ nix run .#hello.copyToDockerDaemon
$ docker run hello:latest
Hello, world!

More Examples

To load and run the bash example image into Podman:

$ nix run github:nlewo/nix2container#examples.bash.copyToPodman
$ podman run -it bash

Functions documentation

nix2container.buildImage

Function arguments are:

nix2container.pullImage

Pull an image from a container registry by name and tag/digest, storing the entirety of the image (manifest and layer tarballs) in a single store path. The supplied sha256 is the narhash of that store path.

Function arguments are:

nix2container.pullImageFromManifest

Pull a base image from a container registry using a supplied manifest file, and the hashes contained within it. The advantages of this over the basic pullImage:

With this function the manifest.json acts as a lockfile meant to be stored in source control alongside the Nix container definitions. As a convenience, the manifest can be fetched/updated using the supplied passthru script, eg:

nix run .#examples.fromImageManifest.fromImage.getManifest > examples/alpine-manifest.json

Function arguments are:

Note that imageTag, os, and arch do not affect the pulled image; that is governed entirely by the supplied manifest.json file. These arguments are used for the manifest-selection logic in the included getManifest script.

Authentication

If the Nix daemon is used for building, here is how to set up registry authentication.

  1. docker login URL to whatever it is
  2. Copy ~/.docker/config.json to /etc/nix/skopeo/auth.json
  3. Make the directory and all the files readable to the nixbld group:
    sudo chmod -R g+rx /etc/nix/skopeo
    sudo chgrp -R nixbld /etc/nix/skopeo
  4. Bind mount the file into the Nix build sandbox
    extra-sandbox-paths = /etc/skopeo/auth.json=/etc/nix/skopeo/auth.json

Every time a new registry authentication has to be added, update /etc/nix/skopeo/auth.json file.

nix2container.buildLayer

For most use cases, this function is not required. However, it could be useful to explicitly isolate some parts of the image in dedicated layers, for caching (see the "Isolate dependencies in dedicated layers" section) or non reproducibility (see the reproducible argument) purposes.

Function arguments are:

Isolate dependencies in dedicated layers

It is possible to isolate application dependencies in a dedicated layer. This layer is built by its own derivation: if storepaths composing this layer don't change, the layer is not rebuilt. Moreover, Skopeo can avoid to push this layer if it has already been pushed.

Let's consider an application printing a conversation. This script depends on bash and the hello binary. Because most of the changes concern the script itself, it would be nice to isolate scripts dependencies in a dedicated layer: when we modify the script, we only need to rebuild and push the layer containing the script. The layer containing dependencies won't be rebuilt and pushed.

As shown below, the buildImage.layers attribute allows to explicitly specify a set of dependencies to isolate.

{ pkgs }:
let
  application = pkgs.writeScript "conversation" ''
    ${pkgs.hello}/bin/hello
    echo "Haaa aa... I'm dying!!!"
  '';
in
pkgs.nix2container.buildImage {
  name = "hello";
  config = {
    entrypoint = ["${pkgs.bash}/bin/bash" application];
  };
  layers = [
    (pkgs.nix2container.buildLayer { deps = [pkgs.bash pkgs.hello]; })
  ];
}

This image contains 2 layers: a layer with bash and hello closures and a second layer containing the script only.

In real life, the isolated layer can contains a Python environment or Node modules.

See Nix & Docker: Layer explicitly without duplicate packages! for learning how to avoid duplicate store paths in your explicitly layered images.

Quick and dirty benchmarks

The main goal of nix2container is to provide fast rebuild/push container cycles. In the following, we provide an order of magnitude of rebuild and repush time, for the uwsgi image.

warning: this is quick and dirty benchmarks which only provide an order of magnitude

We build the container and push the container. We then made a small change in the hello.py file to trigger a rebuild and a push.

Method Rebuild/repush time Executed command
nix2container.buildImage ~1.8s nix run .#example.uwsgi.copyToRegistry
dockerTools.streamLayeredImage ~7.5s nix build .#example.uwsgi \| docker load
dockerTools.buildImage ~10s nix build .#example.uwsgi; skopeo copy docker-archive://./result docker://localhost:5000/uwsgi:latest

Note we could not compare the same distribution mechanisms because

Run the tests

nix run .#tests.all

This builds several example images with Nix, loads them with Skopeo, runs them with Podman, and test output logs.

Not that, unfortunately, these tests are not executed in the Nix sandbox because it is currently not possible to run a container in the Nix sandbox.

It is also possible to run a specific test:

nix run .#tests.basic

The nix2container Go library

This library is currently used by the Skopeo nix transport available in this branch.

For more information, refer to the Go documentation.