Skip to content

Building Software with Nix

This section builds the same tiny program two ways:

  1. Dockerfile using a regular base image (still includes a whole distro)
  2. Nix using dockerTools.buildLayeredImage (image contains only what you ask for)

We’ll then compare image size and layers using standard Docker inspection commands.


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;
}

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" ];
};
};
Terminal window
nix build -L .#hello-nix-image
docker load < result

Run it:

Terminal window
docker run --rm hello-nix:latest ChatGPT

Build 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.

# Build stage
FROM alpine:3.19 AS build
RUN apk add --no-cache build-base
WORKDIR /src
COPY src/hello.c .
RUN cc hello.c -O2 -static -o hello
# Runtime stage
FROM alpine:3.19
COPY --from=build /src/hello /bin/hello
ENTRYPOINT ["/bin/hello"]

Build + run:

Terminal window
docker build -t hello-docker:latest .
docker run --rm hello-docker:latest ChatGPT

Terminal window
docker image ls | rg 'hello-nix|hello-docker'

(If you don’t have rg, use grep.)

Terminal window
docker system df

This shows how big the Nix-produced image artifact is in the store:

Terminal window
nix path-info -Sh .#hello-nix-image

Inspect layers and why “base images” add bloat

Section titled “Inspect layers and why “base images” add bloat”
Terminal window
docker history hello-docker:latest
docker history hello-nix:latest

What 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.)”
Terminal window
docker inspect hello-docker:latest --format '{{json .Config}}' | jq
docker inspect hello-nix:latest --format '{{json .Config}}' | jq

Run a shell inside each image and list what’s there:

Terminal window
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:

Terminal window
docker run --rm -it --entrypoint sh hello-nix:latest -lc 'ls -lah /; ls -lah /bin'

If you have them installed:

Terminal window
dive hello-docker:latest
dive hello-nix:latest
Terminal window
skopeo inspect docker-daemon:hello-docker:latest | jq '.Size, .Layers'
skopeo inspect docker-daemon:hello-nix:latest | jq '.Size, .Layers'