Nix Docker Image Building

Type: Build Tooling Relation: NixOS package manager applied to container image generation

The Nix package manager can build Docker-compatible images without a Dockerfile or Docker daemon at build time.

Core Functions

dockerTools.buildImage

Produces a Docker-compatible tarball loaded with docker load:

pkgs.dockerTools.buildImage {
  name = "my-app";
  tag = "latest";
  contents = [ pkgs.hello ];
  config.Cmd = [ "${pkgs.hello}/bin/hello" ];
}

dockerTools.buildLayeredImage

Separate layers per store path — better for caching and incremental pulls:

pkgs.dockerTools.buildLayeredImage {
  name = "my-app";
  tag = "latest";
  contents = [ pkgs.python3 myApp ];
  config.Cmd = [ "${myApp}/bin/serve" ];
}

dockerTools.streamLayeredImage

Streams the image directly to docker load without materializing the full tarball on disk.

Why This Matters

PropertyDockerfileNix dockerTools
Reproducibilityapt-get update drifts over timeSame expression → same image, bit-for-bit
Image sizeBase OS layer + app layers (100s of MB)Only closure of declared deps (10-50 MB typical)
Build-time depsRequires Docker daemon (or BuildKit)Pure Nix store derivation, no daemon
Layer granularityOne layer per RUN instructionOne layer per store path (with buildLayeredImage)
Imperative stepsRUN shell commands during buildEverything expressed as Nix derivations

Tradeoffs

  • No RUN commands — No imperative shell execution during build. Everything must be expressed as derivations. This is the point, but it’s a different mental model.
  • Learning curve — Nix language + nixpkgs conventions are non-trivial.
  • Debugging — No shell in the image by default (must explicitly add pkgs.bashInteractive).
  • glibc/musl — Nix uses glibc by default. Watch for incompatibility with Alpine-based expectations.

Flake Pattern

# flake.nix
{
  outputs = { self, nixpkgs }: let
    pkgs = nixpkgs.legacyPackages.x86_64-linux;
  in {
    packages.x86_64-linux.docker = pkgs.dockerTools.buildLayeredImage {
      name = "myservice";
      tag = "latest";
      contents = [ self.packages.x86_64-linux.default ];
      config = {
        Cmd = [ "${self.packages.x86_64-linux.default}/bin/myservice" ];
        ExposedPorts."8080/tcp" = {};
      };
    };
  };
}
nix build .#docker
docker load < result
docker run myservice:latest

Styrene Relevance

Nix-built containers complement the NixOS fleet pattern. Where NixOS provides the host OS declaratively, dockerTools extends the same reproducibility guarantees to containerized workloads — useful for services deployed via Komodo or graduated to styrene-lab/<repo> as OCI artifacts.

Resources

  • NixOS — Fleet device OS using the same Nix ecosystem
  • mTLS via Vault PKI — Container security at ingress

Graph