Nixery


Welcome to this instance of Nixery. It provides ad-hoc container images that contain packages from the Nix package manager. Images with arbitrary packages can be requested via the image name.

Nix not only provides the packages to include in the images, but also builds the images themselves by using a special layering strategy that optimises for cache efficiency.

For general information on why using Nix makes sense for container images, check out this blog post.

Demo

Quick start

Simply pull an image from this registry, separating each package you want included by a slash:

docker pull nixery.dev/shell/git/htop

This gives you an image with git, htop and an interactively configured shell. You could run it like this:

docker run -ti nixery.dev/shell/git/htop bash

Each path segment corresponds either to a key in the Nix package set, or a meta-package that automatically expands to several other packages.

Meta-packages must be the first path component if they are used. Currently the only meta-package is shell, which provides a bash-shell with interactive configuration and standard tools like coreutils.

Tip: When pulling from a private Nixery instance, replace nixery.dev in the above examples with your registry address.

FAQ

If you have a question that is not answered here, feel free to file an issue on Github so that we can get it included in this section. The volume of questions is quite low, thus by definition your question is already frequently asked.

Where is the source code for this?

Over on Github. It is licensed under the Apache 2.0 license. Consult the documentation entries in the sidebar for information on how to set up your own instance of Nixery.

Which revision of nixpkgs is used for the builds?

The instance at nixery.dev tracks a recent NixOS channel, currently NixOS 20.09. The channel is updated several times a day.

Private registries might be configured to track a different channel (such as nixos-unstable) or even track a git repository with custom packages.

Should I depend on nixery.dev in production?

While we appreciate the enthusiasm, if you would like to use Nixery in your production project we recommend setting up a private instance. The public Nixery at nixery.dev is run on a best-effort basis and we make no guarantees about availability.

Is this an official Google project?

No. Nixery is not officially supported by Google.

Who made this?

Nixery was written by tazjin, but many people have contributed to Nix over time, maybe you could become one of them?

Under the hood

This page serves as a quick explanation of what happens under-the-hood when an image is requested from Nixery.


1. The image manifest is requested

When container registry clients such as Docker pull an image, the first thing they do is ask for the image manifest. This is a JSON document describing which layers are contained in an image, as well as some additional auxiliary information.

This request is of the form GET /v2/$imageName/manifests/$imageTag.

Nixery receives this request and begins by splitting the image name into its path components and substituting meta-packages (such as shell) for their contents.

For example, requesting shell/htop/git results in Nixery expanding the image name to ["bashInteractive", "coreutils", "htop", "git"].

If Nixery is configured with a private Nix repository, it also looks at the image tag and substitutes latest with master.

It then invokes Nix with three parameters:

  1. image contents (as above)
  2. image tag
  3. configured package set source

2. Nix fetches and prepares image content

Using the parameters above, Nix imports the package set and begins by mapping the image names to attributes in the package set.

A special case during this process is packages with uppercase characters in their name, for example anything under haskellPackages. The registry protocol does not allow uppercase characters, so the Nix code will translate something like haskellpackages (lowercased) to the correct attribute name.

After identifying all contents, Nix uses the symlinkJoin function to create a special layer with the "symlink farm" required to let the image function like a normal disk image.

Nix then returns information about the image contents as well as the location of the special layer to Nixery.

3. Layers are grouped, created, hashed, and persisted

With the information received from Nix, Nixery determines the contents of each layer while optimising for the best possible cache efficiency (see the layering design doc for details).

With the grouped layers, Nixery then begins to create compressed tarballs with all required contents for each layer. As these tarballs are being created, they are simultaneously being hashed (as the image manifest must contain the content-hashes of all layers) and persisted to storage.

Storage can be either a remote Google Cloud Storage bucket, or a local filesystem path.

During this step, Nixery checks its build cache (see [Caching][]) to determine whether a layer needs to be built or is already cached from a previous build.

Note: While this step is running (which can take some time in the case of large first-time image builds), the registry client is left hanging waiting for an HTTP response. Unfortunately the registry protocol does not allow for any feedback back to the user at this point, so from the user's perspective things just ... hang, for a moment.

4. The manifest is assembled and returned to the client

Once armed with the hashes of all required layers, Nixery assembles the OCI Container Image manifest which describes the structure of the built image and names all of its layers by their content hash.

This manifest is returned to the client.

5. Image layers are requested

The client now inspects the manifest and determines which of the layers it is currently missing based on their content hashes. Note that different container runtimes will handle this differently, and in the case of certain engine and storage driver combinations (e.g. Docker with OverlayFS) layers might be downloaded again even if they are already present.

For each of the missing layers, the client now issues a request to Nixery that looks like this:

GET /v2/${imageName}/blob/sha256:${layerHash}

Nixery receives these requests and handles them based on the configured storage backend.

If the storage backend is GCS, it redirects them to Google Cloud Storage URLs, responding with an HTTP 303 See Other status code and the actual download URL of the layer.

Nixery supports using private buckets which are not generally world-readable, in which case signed URLs are constructed using a private key. These allow the registry client to download each layer without needing to care about how the underlying authentication works.

If the storage backend is the local filesystem, Nixery will attempt to serve the layer back to the client from disk.


That's it. After these five steps the registry client has retrieved all it needs to run the image produced by Nixery.

Caching in Nixery

This page gives a quick overview over the caching done by Nixery. All cache data is written to Nixery's storage bucket and is based on deterministic identifiers or content-addressing, meaning that cache entries under the same key never change.

Manifests

Manifests of builds are cached at $BUCKET/manifests/$KEY. The effect of this cache is that multiple instances of Nixery do not need to rebuild the same manifest from scratch.

Since the manifest cache is populated only after layers are uploaded, Nixery can immediately return the manifest to its clients without needing to check whether layers have been uploaded already.

$KEY is generated by creating a SHA1 hash of the requested content of a manifest plus the package source specification.

Manifests are only cached if the package source specification is not a moving target.

Manifest caching only applies in the following cases:

  • package source specification is a specific git commit
  • package source specification is a specific NixOS/nixpkgs commit

Manifest caching never applies in the following cases:

  • package source specification is a local file path (i.e. NIXERY_PKGS_PATH)
  • package source specification is a NixOS channel (e.g. NIXERY_CHANNEL=nixos-20.09)
  • package source specification is a git branch or tag (e.g. staging, master or latest)

It is thus always preferable to request images from a fully-pinned package source.

Manifests can be removed from the manifest cache without negative consequences.

Layer tarballs

Layer tarballs are the files that Nixery clients retrieve from the storage bucket to download an image.

They are stored content-addressably at $BUCKET/layers/$SHA256HASH and layer requests sent to Nixery will redirect directly to this storage location.

The effect of this cache is that Nixery does not need to upload identical layers repeatedly. When Nixery notices that a layer already exists in GCS it will skip uploading this layer.

Removing layers from the cache is potentially problematic if there are cached manifests or layer builds referencing those layers.

To clean up layers, a user must ensure that no other cached resources still reference these layers.

Layer builds

Layer builds are cached at $BUCKET/builds/$HASH, where $HASH is a SHA1 of the Nix store paths included in the layer.

The content of the cached entries is a JSON-object that contains the SHA256 hashes and sizes of the built layer.

The effect of this cache is that different instances of Nixery will not build, hash and upload layers that have identical contents across different instances.

Layer builds can be removed from the cache without negative consequences.

Run your own Nixery


⚠ This page is still under construction! ⚠


Running your own Nixery is not difficult, but requires some setup. Follow the steps below to get up & running.

Note: Nixery can be run inside of a GKE cluster, providing a local service from which images can be requested. Documentation for how to set this up is forthcoming, please see nixery#4.

0. Prerequisites

To run Nixery, you must have:

  • Nix (to build Nixery itself)
  • Somewhere to run it (your own server, Google AppEngine, a Kubernetes cluster, whatever!)
  • A Google Cloud Storage bucket in which to store & serve layers

1. Choose a package set

When running your own Nixery you need to decide which package set you want to serve. By default, Nixery builds packages from a recent NixOS channel which ensures that most packages are cached upstream and no expensive builds need to be performed for trivial things.

However if you are running a private Nixery, chances are high that you intend to use it with your own packages. There are three options available:

  1. Specify an upstream Nix/NixOS channel1, such as nixos-20.09 or nixos-unstable.
  2. Specify your own git-repository with a custom package set2. This makes it possible to pull different tags, branches or commits by modifying the image tag.
  3. Specify a local file path containing a Nix package set. Where this comes from or what it contains is up to you.

2. Build Nixery itself

Building Nixery creates a container image. This section assumes that the container runtime used is Docker, please modify instructions correspondingly if you are using something else.

With a working Nix installation, building Nixery is done by invoking nix-build -A nixery-image from a checkout of the Nixery repository.

This will create a result-symlink which points to a tarball containing the image. In Docker, this tarball can be loaded by using docker load -i result.

3. Prepare configuration

Nixery is configured via environment variables.

You must set all of these:

  • BUCKET: Google Cloud Storage bucket to store & serve image layers
  • PORT: HTTP port on which Nixery should listen

You may set one of these, if unset Nixery defaults to nixos-20.09:

  • NIXERY_CHANNEL: The name of a Nix/NixOS channel to use for building
  • NIXERY_PKGS_REPO: URL of a git repository containing a package set (uses locally configured SSH/git credentials)
  • NIXERY_PKGS_PATH: A local filesystem path containing a Nix package set to use for building

You may set all of these:

  • NIX_TIMEOUT: Number of seconds that any Nix builder is allowed to run (defaults to 60)

To authenticate to the configured GCS bucket, Nixery uses Google's Application Default Credentials. Depending on your environment this may require additional configuration.

If the GOOGLE_APPLICATION_CREDENTIALS environment is configured, the service account's private key will be used to create signed URLs for layers.

4. Deploy Nixery

With the above environment variables configured, you can run the image that was built in step 2.

How this works depends on the environment you are using and is, for now, outside of the scope of this tutorial.

Once Nixery is running you can immediately start requesting images from it.

5. Productionise

(⚠ Here be dragons! ⚠)

Nixery is still an early project and has not yet been deployed in any production environments and some caveats apply.

Notably, Nixery currently does not support any authentication methods, so anyone with network access to the registry can retrieve images.

Running a Nixery inside of a fenced-off environment (such as internal to a Kubernetes cluster) should be fine, but you should consider to do all of the following:

  • Issue a TLS certificate for the hostname you are assigning to Nixery. In fact, Docker will refuse to pull images from registries that do not use TLS (with the exception of .local domains).
  • Configure signed GCS URLs to avoid having to make your bucket world-readable.
  • Configure request timeouts for Nixery if you have your own web server in front of it. This will be natively supported by Nixery in the future.

1

Nixery will not work with Nix channels older than nixos-19.03.

2

This documentation will be updated with instructions on how to best set up a custom Nix repository. Nixery expects custom package sets to be a superset of nixpkgs, as it uses lib and other features from nixpkgs extensively.

Nix

These sections are designed to give some background information on what Nix is. If you've never heard of Nix before looking at Nixery, this might just be the page for you!

Nix is a functional package-manager that comes with a number of advantages over traditional package managers, such as side-by-side installs of different package versions, atomic updates, easy customisability, simple binary caching and much more. Feel free to explore the Nix website for an overview of Nix itself.

Nix uses a custom programming language also called Nix, which is explained here on its own page.

In addition to the package manager and language, the Nix project also maintains NixOS - a Linux distribution built entirely on Nix. On NixOS, users can declaratively describe the entire configuration of their system and perform updates/rollbacks to other system configurations with ease.

Most Nix packages are tracked in the Nix package set, usually simply referred to as nixpkgs. It contains tens of thousands of packages already!

Nixery (which you are looking at!) provides an easy & simple way to get started with Nix, in fact you don't even need to know that you're using Nix to make use of Nixery.

Nix - A One Pager

Nix, the package manager, is built on and with Nix, the language. This page serves as a fast intro to most of the (small) language.

Unless otherwise specified, the word "Nix" refers only to the language below.

Please file an issue if something in here confuses you or you think something important is missing.

Table of Contents

Overview

Nix is:

  • purely functional. It has no concept of sequential steps being executed, any dependency between operations is established by depending on data from previous operations.

    Everything in Nix is an expression, meaning that every directive returns some kind of data.

    Evaluating a Nix expression yields a single data structure, it does not execute a sequence of operations.

    Every Nix file evaluates to a single expression.

  • lazy. It will only evaluate expressions when their result is actually requested.

    For example, the builtin function throw causes evaluation to stop. Entering the following expression works fine however, because we never actually ask for the part of the structure that causes the throw.

    let attrs = { a = 15; b = builtins.throw "Oh no!"; };
    in "The value of 'a' is ${toString attrs.a}"
    
  • purpose-built. Nix only exists to be the language for Nix, the package manager. While people have occasionally used it for other use-cases, it is explicitly not a general-purpose language.

Language constructs

This section describes the language constructs in Nix. It is a small language and most of these should be self-explanatory.

Primitives / literals

Nix has a handful of data types which can be represented literally in source code, similar to many other languages.

# numbers
42
1.72394

# strings & paths
"hello"
./some-file.json

# strings support interpolation
"Hello ${name}"

# multi-line strings (common prefix whitespace is dropped)
''
first line
second line
''

# lists (note: no commas!)
[ 1 2 3 ]

# attribute sets (field access with dot syntax)
{ a = 15; b = "something else"; }

# recursive attribute sets (fields can reference each other)
rec { a = 15; b = a * 2; }

Operators

Nix has several operators, most of which are unsurprising:

SyntaxDescription
+, -, *, /Numerical operations
+String concatenation
++List concatenation
==Equality
>, >=, <, <=Ordering comparators
&&Logical AND
||Logical OR
e1 -> e2Logical implication (i.e. !e1 || e2)
!Boolean negation
set.attrAccess attribute attr in attribute set set
set ? attributeTest whether attribute set contains an attribute
left // rightMerge left & right attribute sets, with the right set taking precedence

Make sure to understand the //-operator, as it is used quite a lot and is probably the least familiar one.

Variable bindings

Bindings in Nix are introduced locally via let expressions, which make some variables available within a given scope.

For example:

let
  a = 15;
  b = 2;
in a * b

# yields 30

Variables are immutable. This means that after defining what a or b are, you can not modify their value in the scope in which they are available.

You can nest let-expressions to shadow variables.

Variables are not available outside of the scope of the let expression. There are no global variables.

Functions

All functions in Nix are anonymous lambdas. This means that they are treated just like data. Giving them names is accomplished by assigning them to variables, or setting them as values in an attribute set (more on that below).

# simple function
# declaration is simply the argument followed by a colon
name: "Hello ${name}"

Multiple arguments (currying)

Technically any Nix function can only accept one argument. Sometimes however, a function needs multiple arguments. This is achieved in Nix via currying, which means to create a function with one argument, that returns a function with another argument, that returns ... and so on.

For example:

name: age: "${name} is ${toString age} years old"

An additional benefit of this approach is that you can pass one parameter to a curried function, and receive back a function that you can re-use (similar to partial application):

let
  multiply = a: b: a * b;
  doubleIt = multiply 2; # at this point we have passed in the value for 'a' and
                         # receive back another function that still expects 'b'
in
  doubleIt 15

# yields 30

Multiple arguments (attribute sets)

Another way of specifying multiple arguments to a function in Nix is to make it accept an attribute set, which enables multiple other features:

{ name, age }: "${name} is ${toString age} years old"

Using this method, we gain the ability to specify default arguments (so that callers can omit them):

{ name, age ? 42 }: "${name} is ${toString age} years old"

Or in practice:

let greeter =  { name, age ? 42 }: "${name} is ${toString age} years old";
in greeter { name = "Slartibartfast"; }

# yields "Slartibartfast is 42 years old"
# (note: Slartibartfast is actually /significantly/ older)

Additionally we can introduce an ellipsis using ..., meaning that we can accept an attribute set as our input that contains more variables than are needed for the function.

let greeter = { name, age, ... }: "${name} is ${toString age} years old";
    person = {
      name = "Slartibartfast";
      age = 42;
      # the 'email' attribute is not expected by the 'greeter' function ...
      email = "slartibartfast@magrath.ea";
    };
in greeter person # ... but the call works due to the ellipsis.

if ... then ... else ...

Nix has simple conditional support. Note that if is an expression in Nix, which means that both branches must be specified.

if someCondition
then "it was true"
else "it was false"

inherit keyword

The inherit keyword is used in attribute sets or let bindings to "inherit" variables from the parent scope.

In short, a statement like inherit foo; expands to foo = foo;.

Consider this example:

let
  name = "Slartibartfast";
  # ... other variables
in {
  name = name; # set the attribute set key 'name' to the value of the 'name' var
  # ... other attributes
}

The name = name; line can be replaced with inherit name;:

let
  name = "Slartibartfast";
  # ... other variables
in {
  inherit name;
  # ... other attributes
}

This is often convenient, especially because inherit supports multiple variables at the same time as well as "inheritance" from other attribute sets:

{
  inherit name age; # equivalent to `name = name; age = age;`
  inherit (otherAttrs) email; # equivalent to `email = otherAttrs.email`;
}

with statements

The with statement "imports" all attributes from an attribute set into variables of the same name:

let attrs = { a = 15; b = 2; };
in with attrs; a + b # 'a' and 'b' become variables in the scope following 'with'

import / NIX_PATH / <entry>

Nix files can import each other by using the import keyword and a literal path:

# assuming there is a file lib.nix with some useful functions
let myLib = import ./lib.nix;
in myLib.usefulFunction 42

Nix files often begin with a function header to pass parameters into the rest of the file, so you will often see imports of the form import ./some-file { ... }.

Nix has a concept of a NIX_PATH (similar to the standard PATH environment variable) which contains named aliases for file paths containing Nix expressions.

In a standard Nix installation, several channels will be present (for example nixpkgs or nixos-unstable) on the NIX_PATH.

NIX_PATH entries can be accessed using the <entry> syntax, which simply evaluates to their file path:

<nixpkgs>
# might yield something like `/home/tazjin/.nix-defexpr/channels/nixpkgs`

This is commonly used to import from channels:

let pkgs = import <nixpkgs> {};
in pkgs.something

Standard libraries

Yes, libraries, plural.

Nix has three major things that could be considered its standard library and while there's a lot of debate to be had about this point, you still need to know all three.

builtins

Nix comes with several functions that are baked into the language. These work regardless of which other Nix code you may or may not have imported.

Most of these functions are implemented in the Nix interpreter itself, which means that they are rather fast when compared to some of the equivalents which are implemented in Nix itself.

The Nix manual has a section listing all builtins and their usage.

Examples of builtins that you will commonly encounter include, but are not limited to:

  • derivation (see Derivations)
  • toJSON / fromJSON
  • toString
  • toPath / fromPath

The builtins also include several functions that have the (spooky) ability to break Nix' evaluation purity. No functions written in Nix itself can do this.

Examples of those include:

  • fetchGit which can fetch a git-repository using the environment's default git/ssh configuration
  • fetchTarball which can fetch & extract archives without having to specify hashes

Read through the manual linked above to get the full overview.

pkgs.lib

The Nix package set, commonly referred to by Nixers simply as nixpkgs, contains a child attribute set called lib which provides a large number of useful functions.

The canonical definition of these functions is their source code. I wrote a tool (nixdoc) in 2018 which generates manual entries for these functions, however not all of the files are included as of July 2019.

See the Nixpkgs manual entry on lib for the documentation.

These functions include various utilities for dealing with the data types in Nix (lists, attribute sets, strings etc.) and it is useful to at least skim through them to familiarise yourself with what is available.

{ pkgs ? import <nixpkgs> {} }:

with pkgs.lib; # bring contents pkgs.lib into scope

strings.toUpper "hello"

# yields "HELLO"

pkgs itself

The Nix package set itself does not just contain packages, but also many useful functions which you might run into while creating new Nix packages.

One particular subset of these that stands out are the trivial builders, which provide utilities for writing text files or shell scripts, running shell commands and capturing their output and so on.

{ pkgs ? import <nixpkgs> {} }:

pkgs.writeText "hello.txt" "Hello dear reader!"

# yields a derivation which creates a text file with the above content

Derivations

When a Nix expression is evaluated it may yield one or more derivations. Derivations describe a single build action that, when run, places one or more outputs (whether they be files or folders) in the Nix store.

The builtin function derivation is responsible for creating derivations at a lower level. Usually when Nix users create derivations they will use the higher-level functions such as stdenv.mkDerivation.

Please see the manual on derivations for more information, as the general build logic is out of scope for this document.

Nix Idioms

There are several idioms in Nix which are not technically part of the language specification, but will commonly be encountered in the wild.

This section is an (incomplete) list of them.

File lambdas

It is customary to start every file with a function header that receives the files dependencies, instead of importing them directly in the file.

Sticking to this pattern lets users of your code easily change out, for example, the specific version of nixpkgs that is used.

A common file header pattern would look like this:

{ pkgs ? import <nixpkgs> {} }:

# ... 'pkgs' is then used in the code

In some sense, you might consider the function header of a file to be its "API".

callPackage

Building on the previous pattern, there is a custom in nixpkgs of specifying the dependencies of your file explicitly instead of accepting the entire package set.

For example, a file containing build instructions for a tool that needs the standard build environment and libsvg might start like this:

# my-funky-program.nix
{ stdenv, libsvg }:

stdenv.mkDerivation { ... }

Any time a file follows this header pattern it is probably meant to be imported using a special function called callPackage which is part of the top-level package set (as well as certain subsets, such as haskellPackages).

{ pkgs ? import <nixpkgs> {} }:

let my-funky-program = callPackage ./my-funky-program.nix {};
in # ... something happens with my-funky-program

The callPackage function looks at the expected arguments (via builtins.functionArgs) and passes the appropriate keys from the set in which it is defined as the values for each corresponding argument.

Overrides / Overlays

One of the most powerful features of Nix is that the representation of all build instructions as data means that they can easily be overridden to get a different result.

For example, assuming there is a package someProgram which is built without our favourite configuration flag (--mimic-threaten-tag) we might override it like this:

someProgram.overrideAttrs(old: {
    configureFlags = old.configureFlags ++ ["--mimic-threaten-tag"];
})

This pattern has a variety of applications of varying complexity. The top-level package set itself can have an overlays argument passed to it which may add new packages to the imported set.

For a slightly more advanced example, assume that we want to import <nixpkgs> but have the modification above be reflected in the imported package set:

let
  overlay = (self: super: {
    someProgram = super.someProgram.overrideAttrs(old: {
      configureFlags = old.configureFlags ++ ["--mimic-threaten-tag"];
    });
  });
in import <nixpkgs> { overlays = [ overlay ]; }

The overlay function receives two arguments, self and super. self is the fixed point of the overlay's evaluation, i.e. the package set including the new packages and super is the "original" package set.

See the Nix manual sections on overrides and on overlays for more details.