Asko Soukka

Building Docker containers from scratch using Nix

Nix makes it reasonable to build Docker containers from scratch. The resulting containers are still big (yet I heard there’s ongoing work to make Nix builds more lean), but at least you don’t need to think about choosing and keeping the base images up to date.

Next follows an example, how to make a Docker image for Plone with Nix.

Creating Nix expression with collective.recipe.nix

At first, we need Nix expression for Plone. Here I use one built with my buildout based generator, collective.recipe.nix. It generates a few exression, including plone.nix and plone-env.nix. The first one is only really usable with nix-shell, but the other one can be used building a standalone Plone for Docker image.

To create ./plone-env.nix, I need a buildout environment in ./default.nix:

with import <nixpkgs> {}; {
  myEnv = stdenv.mkDerivation {
    name = "myEnv";
    buildInputs = [
      pythonPackages.buildout
    ];
    shellHook = ''
      export SSL_CERT_FILE=~/.nix-profile/etc/ca-bundle.crt
    '';
  };
}

And a minimal Plone buildout using my recipe in ./buildout.cfg:

[buildout]
extends = https://dist.plone.org/release/4-latest/versions.cfg
parts = plone
versions = versions

[instance]
recipe = plone.recipe.zope2instance
eggs = Plone
user = admin:admin

[plone]
recipe = collective.recipe.nix
eggs =
    ${instance:eggs}
    plone.recipe.zope2instance

[versions]
zc.buildout =
setuptools =

And finally produce both plone.nix and the required plone-env.nix with:

$ nix-shell --run buildout

Creating Docker container with Nix Docker buildpack

Next up is building the container with our Nix expression with the help of a builder container, which I call Nix Docker buildpack.

At first, we need to clone that:

$ git clone https://github.com/datakurre/nix-build-pack-docker
$ cd nix-build-pack-docker

And build the builder:

$ cd builder
$ docker build -t nix-build-pack --rm=true --force-rm=true --no-cache=true .
$ cd ..

Now the builder can be used to build a tarball, which only contains the built Nix derivation Plone. Let’s copy the created plone-env.nix into the current working directory and run:

$ docker run --rm -v `pwd`:/opt nix-build-pack /opt/plone-env.nix

After a while, that directory should contain file called plone-env.nix.tar.gz, which only contains two directories in its root: /nix for the built derivation and /app for easy access symlinks, like /app/bin/python.

Now we need ./Dockerfile for building the final Plone image:

FROM scratch
ADD plone.env.nix.tar.gz /
EXPOSE 8080
USER 1000
ENTRYPOINT ["/app/bin/python"]

And finally, a Plone image can be built with

$ docker build -t plone --rm=true --force-rm=true --no-cache=true .

Running Nix-built Plone container

To run Plone in a container with the image built above, we still need the configuration for Plone. We can the normal buildout generated configuration, but we need to

  1. remove site.py from parts/instance.
  2. fix paths to match in parts/instance/zope.conf to match the mounted paths in Docker container (/opt/...)
  3. create some temporary directory to be mounted into container

Also, we need a small wrapper to call the Plone instance script, ./instance.py, because we cannot use the buildout generated one:

import sys
import plone.recipe.zope2instance.ctl

sys.exit(plone.recipe.zope2instance.ctl.main(
    ['-C', '/opt/parts/instance/etc/zope.conf']
    + sys.argv[1:]
))

When these are in place, within the buildout directory, we should now be able to run Plone in Docker container with:

$ docker run --rm -v `pwd`:/opt -v `pwd`/tmp:/tmp -P plone /opt/instance.py fg

The current working directory is mapped to /opt and some temporary directory is mapped to /tmp (because our image didn’t contain even a [/tmp]{.title-ref}).

Note: When I tried this out, for some reason (possibly because VirtualBox mount with boot2docker), I had to remove ./var/filestorage/Data.fs.tmp between runs or I got errors on ZODB writes.