Building Software with Nix
Minimal Docker Images: Nix vs Dockerfile
Section titled “Minimal Docker Images: Nix vs Dockerfile”This section builds the same tiny program two ways:
- Dockerfile using a regular base image (still includes a whole distro)
- Nix using
dockerTools.buildLayeredImage(image contains only what you ask for)
We’ll then compare image size and layers using standard Docker inspection commands.
The example program
Section titled “The example program”Create src/hello.c:
#include <stdio.h>
int main(int argc, char *argv[]) { const char *name = (argc > 1) ? argv[1] : "world"; printf("hello, %s!\n", name); return 0;}Build a minimal Docker image with Nix
Section titled “Build a minimal Docker image with Nix”flake.nix (relevant excerpt)
Section titled “flake.nix (relevant excerpt)”This builds a static C binary and puts only that binary into the image.
# Build a static binary so the image doesn't need glibc, etc. hello-static = pkgs.pkgsStatic.stdenv.mkDerivation { pname = "hello-static"; version = "1.0"; src = ./src;
buildPhase = '' gcc hello.c -O2 -static -o hello '';
installPhase = '' mkdir -p $out/bin install -m755 hello $out/bin/hello ''; };
hello-nix-image = pkgs.dockerTools.buildLayeredImage { name = "hello-nix"; tag = "latest";
# Only include our binary. contents = [ hello-static ];
config = { Cmd = [ "/bin/hello" ]; }; };Build + load the image
Section titled “Build + load the image”nix build -L .#hello-nix-imagedocker load < resultRun it:
docker run --rm hello-nix:latest ChatGPTBuild a comparable Docker image with Dockerfile
Section titled “Build a comparable Docker image with Dockerfile”A “normal” Docker build typically starts from a base image (e.g. Alpine, Debian, Ubuntu). Even “minimal” base images still include lots of filesystem + package manager bits.
Dockerfile
Section titled “Dockerfile”# Build stageFROM alpine:3.19 AS buildRUN apk add --no-cache build-baseWORKDIR /srcCOPY src/hello.c .RUN cc hello.c -O2 -static -o hello
# Runtime stageFROM alpine:3.19COPY --from=build /src/hello /bin/helloENTRYPOINT ["/bin/hello"]Build + run:
docker build -t hello-docker:latest .docker run --rm hello-docker:latest ChatGPTCompare size
Section titled “Compare size”Quick size overview
Section titled “Quick size overview”docker image ls | rg 'hello-nix|hello-docker'(If you don’t have rg, use grep.)
Disk usage summary
Section titled “Disk usage summary”docker system dfNix output size (store size)
Section titled “Nix output size (store size)”This shows how big the Nix-produced image artifact is in the store:
nix path-info -Sh .#hello-nix-imageInspect layers and why “base images” add bloat
Section titled “Inspect layers and why “base images” add bloat”Show layer history (size per layer)
Section titled “Show layer history (size per layer)”docker history hello-docker:latestdocker history hello-nix:latestWhat to look for:
- The Dockerfile version will include a layer for the base image filesystem
- The Nix version should be tiny: mostly just your binary (and maybe metadata)
Inspect image metadata (Entrypoint/Cmd, etc.)
Section titled “Inspect image metadata (Entrypoint/Cmd, etc.)”docker inspect hello-docker:latest --format '{{json .Config}}' | jqdocker inspect hello-nix:latest --format '{{json .Config}}' | jqExplore the filesystem contents quickly
Section titled “Explore the filesystem contents quickly”Run a shell inside each image and list what’s there:
docker run --rm -it --entrypoint sh hello-docker:latest -lc 'ls -lah /; ls -lah /bin | head'The Nix image won’t have sh (because we didn’t include it). That’s the point:
it contains only what you asked for.
If you want an interactive shell in the Nix image for inspection, add it explicitly:
contents = [ hello-static pkgs.busybox ];config = { Cmd = [ "/bin/hello" ]; };Then rebuild and you can:
docker run --rm -it --entrypoint sh hello-nix:latest -lc 'ls -lah /; ls -lah /bin'Optional: Deep layer inspection tools
Section titled “Optional: Deep layer inspection tools”If you have them installed:
dive (interactive layer explorer)
Section titled “dive (interactive layer explorer)”dive hello-docker:latestdive hello-nix:latestskopeo (inspect without running)
Section titled “skopeo (inspect without running)”skopeo inspect docker-daemon:hello-docker:latest | jq '.Size, .Layers'skopeo inspect docker-daemon:hello-nix:latest | jq '.Size, .Layers'