Nix & Kubernetes: Reproducible Containers

Hey there, fellow engineers! Ever found yourself wrestling with inconsistent builds, “it works on my machine” syndrome, or just plain old dependency hell? If you’re building applications for Kubernetes, these headaches can multiply. What if there was a way to achieve truly reproducible environments from development to production, ensuring every dependency, every library, and every configuration is exactly what you expect? Enter Nix – a powerful package manager and build system that’s gaining serious traction.

Today, we’re going to explore how Nix can become your secret weapon for building and deploying rock-solid applications to Kubernetes. We’ll dive into why Nix is such a game-changer, the practical strategies for integrating it with your containerized world, and best practices to make your workflow smoother than ever.

The Nix Advantage: Consistency is King

At its heart, Nix is about declarative and reproducible builds. Unlike traditional package managers that install software globally, Nix builds every package in isolation, storing it in its own unique path in the /nix/store. This seemingly simple architectural choice unlocks a cascade of benefits:

  • Reproducibility: If you define a build process with Nix, anyone running that definition will get the exact same output. No more “works on my machine” because the build environment itself is precisely defined.
  • Atomic Upgrades and Rollbacks: Because packages are isolated, upgrading or rolling back a component won’t break other parts of your system. You can switch between different versions of an application or its dependencies instantly.
  • Hermetic Builds: Nix builds are hermetic, meaning they don’t depend on anything outside their explicit inputs. This eliminates subtle bugs caused by ambient system libraries or environment variables.
  • Multi-versioning: You can have multiple versions of the same library or application installed simultaneously without conflicts. This is a dream for developers working on several projects with differing requirements.

Nix package manager logo with developer working
Photo by Mockup Free on Unsplash

These features make Nix incredibly appealing for developing robust, production-ready software. But how do we bring this level of deterministic control to a dynamic, container-orchestrated environment like Kubernetes?

Bridging the Gap: Nix’s Role in a Kubernetes World

Kubernetes thrives on containers – isolated, portable units of software. Traditionally, these containers are built using tools like Docker, often relying on Dockerfiles that specify layers, installations, and configurations. While Dockerfiles are powerful, they can suffer from non-hermetic builds, leading to different images being produced from the same Dockerfile over time due to external changes (e.g., updated base images, shifting upstream package repositories).

This is where Nix steps in. Instead of seeing Nix as a replacement for containers, think of it as a superior container image builder. Nix allows you to define your entire application, its dependencies, and its runtime environment in a declarative way, then package that into a standard OCI (Open Container Initiative) compatible image that Kubernetes understands perfectly.

Strategy 1: Building OCI Images with Nix

The most effective and widely adopted way to run Nix-based environments in Kubernetes is to use Nix to build your container images. This leverages Nix’s strengths for dependency management and reproducibility while adhering to Kubernetes’ fundamental unit of deployment: the container.

Nix provides excellent tools within nixpkgs for this exact purpose, particularly dockerTools. These tools allow you to construct minimal, reproducible container images directly from your Nix expressions.

Let’s look at a simplified example using Nix flakes for a basic web application:

# flake.nix
{
  description = "A simple web application built with Nix for Kubernetes";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  };

  outputs = { self, nixpkgs }:
    let
      pkgs = nixpkgs.legacyPackages.x86_64-linux;
      appName = "my-web-app";
      appVersion = "0.1.0";

      # Define our simple web application
      myWebApp = pkgs.stdenv.mkDerivation {
        pname = appName;
        version = appVersion;
        src = ./.; # Assuming your app source is in the same directory

        buildInputs = [ pkgs.nodejs ]; # Example: if your app uses Node.js

        # Example build phase (replace with your actual build commands)
        buildPhase = ''
          echo "Building ${appName}..."
          mkdir $out
          echo "console.log('Hello from Nix and Kubernetes!');" > $out/app.js
        '';

        installPhase = ''
          cp -r $out $out/bin
        '';
      };

    in {
      # This output defines our OCI image
      packages.x86_64-linux.myWebAppImage = pkgs.dockerTools.buildImage {
        name = appName;
        tag = appVersion;
        contents = [
          myWebApp
          pkgs.nodejs # Include runtime dependencies in the image
        ];
        config = {
          Cmd = [ "${myWebApp}/bin/app.js" ];
          ExposedPorts = { "8080/tcp" = {}; };
        };
      };
    };
}

This flake defines a simple web application and builds it into a Docker-compatible image. You’d build this image with nix build .#myWebAppImage, which produces a tarball you can load into Docker (docker load < result) or push to a container registry that Kubernetes can pull from.

The Benefits Are Clear

Building images this way brings several advantages over traditional Dockerfiles:

  • Bit-for-bit reproducibility: The same input always produces the exact same output
  • Layering optimization: Nix automatically creates efficient layers based on dependency relationships
  • Minimal images: Only include what you actually need – no OS bloat
  • Dependency isolation: Each package gets its own /nix/store path, preventing conflicts

Strategy 2: NixOS Containers and System Declarations

For more complex deployments, you can leverage NixOS containers as an alternative. NixOS allows you to declare entire system configurations declaratively, including services, networking, and environment setup. While running full NixOS inside Kubernetes pods isn’t common practice, you can use nixos-generators to create container images from NixOS configurations.

This approach shines when you need complex multi-service setups or want to ensure your entire runtime environment – not just your application – is reproducible.

Integrating with Your Kubernetes Workflow

Once you’ve built your Nix-based container images, deploying them to Kubernetes is straightforward. Your workflow might look like:

  1. Build the image with Nix (locally or in CI/CD)
  2. Push to a registry (Docker Hub, GitHub Container Registry, your private registry)
  3. Deploy with standard Kubernetes manifests (Deployments, Services, etc.)

Here’s a simple Kubernetes deployment that references a Nix-built image:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nix-web-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nix-web-app
  template:
    metadata:
      labels:
        app: nix-web-app
    spec:
      containers:
      - name: app
        image: myregistry.io/my-web-app:0.1.0
        ports:
        - containerPort: 8080

Nothing about the Kubernetes side needs to change – Nix operates purely at the image-building layer. Your operations team doesn’t need to know you’re using Nix; they just see a standard OCI image.

Best Practices for Nix + Kubernetes

Through practical experience, several best practices have emerged:

Pin Your Dependencies

Always use flakes or explicitly pin your nixpkgs version. This ensures your builds remain reproducible over time. A specific nixpkgs commit guarantees the same package versions.

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
  };
}

Keep Images Small

While Nix makes it easy to include everything, resist the temptation. Use buildLayeredImage instead of buildImage for better layer caching, and carefully select only the runtime dependencies you need.

Leverage CI/CD Integration

Set up your CI/CD pipeline (GitHub Actions, GitLab CI, etc.) to build images with Nix. The install-nix-action makes this trivial for GitHub Actions.

Use Binary Caches

Set up a binary cache (like Cachix) to speed up builds. Since Nix builds are reproducible, you can safely cache build outputs and share them across your team and CI systems.

Real-World Benefits

Teams adopting Nix for Kubernetes deployments report tangible improvements:

  • Eliminated “works on my machine” issues – development and production environments are truly identical
  • Faster onboarding – new developers get reproducible environments instantly
  • Reduced debugging time – when builds are deterministic, mysterious bugs disappear
  • Improved security – pin exact versions of all dependencies, making vulnerability management straightforward

The Learning Curve is Worth It

Nix has a reputation for being complex, and that’s partly true – it introduces new concepts and a functional programming approach to package management. However, the investment pays dividends. Start small: build a single application image with Nix, deploy it to Kubernetes, and experience the difference. Once you see the benefits firsthand, expanding Nix usage becomes a natural progression.

Conclusion

Combining Nix with Kubernetes gives you the best of both worlds: reproducible, deterministic builds from Nix and dynamic, scalable orchestration from Kubernetes. You’re no longer at the mercy of upstream package repositories changing, base images shifting, or environmental differences between systems.

For teams serious about reliability, security, and developer productivity, this combination is increasingly becoming the standard. The initial learning curve is real, but the long-term benefits – in reduced bugs, faster debugging, and rock-solid reproducibility – make it a worthwhile investment.

If you’re ready to eliminate the chaos of inconsistent builds, start exploring Nix today. Your future self (and your operations team) will thank you.

References

[1] NixOS Foundation. NixOS Manual. Available at: https://nixos.org/manual/nix/stable/

[2] NixOS Community. Nixpkgs Manual - dockerTools. Available at: https://nixos.org/manual/nixpkgs/stable/#sec-pkgs-dockerTools

[3] Kubernetes Documentation. Container Images. Available at: https://kubernetes.io/docs/concepts/containers/images/

[4] Dolstra, E. (2006). The Purely Functional Software Deployment Model. PhD Thesis, Utrecht University. Available at: https://edolstra.github.io/pubs/phd-thesis.pdf

Thank you for reading! If you have any feedback or comments, please send them to [email protected].