[coralynn@cyn:/var/www/blog/nix-docker-cache]$ man build-cache

To take advantage of caching in Docker, the inputs at a given step need to be identical to a previous build. By seperating the Dockerfile into two steps, we can cache the build inputs, and in my case cut the build time by 30 minutes.

To do this Nix has a trick we can use to pull dependencies without building the source. Meaning we only need to copy in the Nix file, which should remain stable unless a dependency has been modified. nix-shell some.nix --run exit will gather everything needed to make the derivation, and exit without building.

Copy only the nix file in and run the command:

COPY default.nix .

RUN nix-shell default.nix --run exit

If your Nix file relies on an external file for build inputs, eg: callCabal2nix for Haskell; copy the files needed. Importantly only copy files absolutely needed to fetch the build inputs, and prefer structuring them so they change infrequently. This will keep the cache valid longer, and idealy only be invalidated when an input has changed.

An example with callCabal2nix:

COPY site.cabal default.nix .

RUN nix-shell default.nix --run exit

Pinning the nixpkgs for the build also helps. Making sure that you get the dependencies you expect, and invalidating the cache when they are updated.

For my Haskell project the default.nix looks similar to this:

{ pkgs ? import (builtins.fetchTarball "tarball.url") { }, ghc ? "ghc98", ... }:

pkgs.haskell.packages.${ghc}.callPackage "site" ./. {}

Having cached this dependencies now, copy in the source required to build the app and continue as normal, for me this is:

COPY app app
COPY LICENSE .

RUN nix-build -o /site