ociTools in NixOS


⇠ back | date: 2019-09-09 | tags: /dev/diary, NixOS, Virtualisation | duration: 02:30 minutes

With the release of NixOS 19.09 any second now, I thought I wanted to blog about something that I've been working on, that recently made it into master, and thus the new stable channel.

What are OCI tools?

Open Container Initiative (or OCI) produced a spec that standardised what format containers should use. It is implemented by a bunch of runners, such as runc (the Docker/ standard Kubernetes backend) and railcar (more to that later) and outlines in exactly what format a containers metadata and filesystem are to be stored, so to achieve the largest possible reusability.

The spec is pretty long and in some places not very great. There's even a blog post from Oracle, talking about how implementing an OCI runner in Rust made them find bugs in the specification.

What are ociTools?

So now the question is, what does that have to do with NixOS/nixpkgs. The answer is simple: I wanted to be able to containerise single applications on my server, without requiring a container daemon (such as docker) or relying on externally built "Docker containers" from a registry.

So, ociTools.buildContainer was recently merged into nixpkgs/master, allowing you to do exactly that. It's usage is farely straight forward

with pkgs; ociTools.buildContainer {
  args = [
    (writeShellScript "run.sh" ''
      ${hello}/bin/hello -g "Hello from OCI container!"
    '').outPath
  ];
}

The args parameter refers to a list of paths and arguments that are handed to a container runner to run as init. In this case it's creating a shell script with some commands in it, then getting the output derivation path. Alternatively, if you only want to run a single application, you can pass it <package>.outPath directly instead.

There's other options available, such as the os, arch and readonly flags (which aren't very interesting and have sane defaults). Additionally to that there's mounts.

Simply specify any bind-mount you wish to setup at container init in a similar way you would describe your filesystem with nix already:

with pkgs; ociTools.buildContainer {
  args = [
    (writeShellScript "run.sh" ''
      ${hello}/bin/hello -g "Hello from OCI container!"
    '').outPath
  ];
  mounts."/data" = {
    source = "/var/lib/mydata";
  };
}

Railcar + ociTools

So that's all nice and good. But what about actually running these containers. Well, as I previously said I didn't want to have a dependency on a management daemon such as docker. Instead, I also added a module for the afromentioned railcar container runner (Oracle please merge my PR, thank you).

It wraps very cleanly around ociTools and generates systemd units to start containers, restarting them if they crash. This way you can express applications purely in nix, give them access to only the things they need, and be sure that their configuration is in line with the rest of your system rebuild.

services.railcar = {
  enable = true;
  containers = {
    "hello" = {
      cmd = ''
        ${pkgs.hello}/bin/hello -g "Hello railcar!"
      '';
    };
  };
};

The metadata interface for mounts, etc is the same for railcar as for ociTools.

Anyway, I hope you enjoy. There is definitely things to improve, especially considering the vastness of the OCI spec. Plus, at the moment ociTools does require a bunch of manual setup work for an application to function, if it, say, runs a webserver. It would be cool if some NixOS modules could be re-used to make this configuration easier. But I'm sure someone else is gonna have fun figuring that out.