All Articles

Nix for DevOps — Reproducible Development Environments Complete Guide (2026)

Complete guide to using Nix and Nix flakes for reproducible DevOps environments. Covers installation, dev shells, CI/CD integration, Docker image building with Nix, and team adoption strategies.

DevOpsBoysMar 28, 20266 min read
Share:Tweet

"Works on my machine" is a joke. "Works in my Nix shell" is a guarantee.

Every DevOps team has this problem: developer A has Terraform 1.7, developer B has 1.8. The CI pipeline uses 1.6. Nobody's kubectl version matches the cluster. Python dependencies conflict. One engineer's Node.js version breaks the build.

Nix solves this completely. It's a package manager and build system that guarantees reproducible environments — the same tools, same versions, same configuration, everywhere. Every time.

What Is Nix?

Nix is three things:

  1. A package manager — like apt or brew, but reproducible. Every package is stored with its exact dependency tree, so two versions of the same tool never conflict.
  2. A build system — can build any software reproducibly from source
  3. A configuration language — the Nix language describes packages, environments, and systems

The key insight: Nix stores everything in /nix/store with content-addressed paths:

/nix/store/abc123-terraform-1.7.5/
/nix/store/def456-terraform-1.8.2/

Both versions coexist. No conflicts. No PATH hacks. No version managers.

Installing Nix

bash
# Linux/macOS (multi-user installation, recommended)
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
 
# Verify
nix --version

Enable flakes (modern Nix interface):

bash
# Already enabled by Determinate installer
# If not, add to ~/.config/nix/nix.conf:
echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf

Use Case 1: Dev Shell — One Command, All Tools

Create a flake.nix in your project root:

nix
{
  description = "DevOps project environment";
 
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
    flake-utils.url = "github:numtide/flake-utils";
  };
 
  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in
      {
        devShells.default = pkgs.mkShell {
          buildInputs = with pkgs; [
            # Infrastructure
            terraform
            opentofu
            ansible
            packer
 
            # Kubernetes
            kubectl
            kubernetes-helm
            kustomize
            argocd
            k9s
 
            # Cloud CLIs
            awscli2
            google-cloud-sdk
 
            # Languages
            go_1_22
            nodejs_20
            python312
            python312Packages.pip
 
            # Tools
            jq
            yq
            grpcurl
            dive  # Docker image analyzer
            trivy # Security scanner
            pre-commit
          ];
 
          shellHook = ''
            echo "DevOps environment loaded"
            echo "Terraform: $(terraform version -json | jq -r '.terraform_version')"
            echo "kubectl:   $(kubectl version --client -o json | jq -r '.clientVersion.gitVersion')"
            echo "Helm:      $(helm version --short)"
          '';
        };
      });
}

Now any developer runs:

bash
cd my-project
nix develop

And gets the exact same tools at the exact same versions. No "please install terraform 1.7.5." No "make sure you have the right kubectl." It just works.

Use Case 2: Pin Exact Versions

Need specific versions? Pin them:

nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
    # Pin to a specific commit for exact versions
    nixpkgs-terraform.url = "github:NixOS/nixpkgs/abc123def";
  };
 
  outputs = { self, nixpkgs, nixpkgs-terraform, ... }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
      pkgs-tf = nixpkgs-terraform.legacyPackages.${system};
    in
    {
      devShells.${system}.default = pkgs.mkShell {
        buildInputs = [
          pkgs-tf.terraform  # Exact version from pinned commit
          pkgs.kubectl
          pkgs.kubernetes-helm
        ];
      };
    };
}

The flake.lock file (auto-generated) records the exact commit hash for every input. Commit this to Git and every developer gets identical versions forever.

Use Case 3: direnv Integration — Automatic Shell Activation

Nobody wants to remember to run nix develop. Use direnv to auto-activate:

bash
# Install direnv
nix profile install nixpkgs#direnv
 
# Add to your shell rc
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc

Create .envrc in your project:

bash
use flake

Now when you cd into the project, the Nix shell activates automatically. When you leave, it deactivates. Zero friction.

bash
$ cd ~/projects/infra
direnv: loading .envrc
direnv: using flake
DevOps environment loaded
Terraform: 1.7.5
kubectl:   v1.29.3
Helm:      v3.14.2
 
$ cd ~
direnv: unloading

Use Case 4: Build Docker Images with Nix

Nix can build minimal, reproducible Docker images without a Dockerfile:

nix
# flake.nix (add to outputs)
packages.${system}.docker-image = pkgs.dockerTools.buildLayeredImage {
  name = "my-app";
  tag = "latest";
  contents = [
    pkgs.bash
    pkgs.coreutils
    pkgs.curl
    self.packages.${system}.my-app  # Your built application
  ];
  config = {
    Cmd = [ "/bin/my-app" ];
    ExposedPorts = { "8080/tcp" = {}; };
    Env = [ "PORT=8080" ];
  };
};
bash
nix build .#docker-image
docker load < result
docker run -p 8080:8080 my-app:latest

Benefits over Dockerfile:

  • Reproducible — same image every time, bit-for-bit
  • Minimal — only includes exactly what you specify, no base image bloat
  • No layer caching issues — Nix handles caching at the package level
  • Auditable — you can trace every byte in the image back to its source

Use Case 5: CI/CD with Nix

GitHub Actions with Nix:

yaml
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
 
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - uses: DeterminateSystems/nix-installer-action@main
      - uses: DeterminateSystems/magic-nix-cache-action@main
 
      - name: Build
        run: nix build
 
      - name: Test
        run: nix develop --command npm test
 
      - name: Lint
        run: nix develop --command npm run lint
 
      - name: Build Docker image
        run: nix build .#docker-image

The magic-nix-cache action caches Nix store paths, so subsequent builds are fast.

The killer feature: your CI uses the exact same tool versions as your local dev environment. No more "tests pass locally but fail in CI" because of version differences.

Use Case 6: Multi-Environment Shells

Different projects need different tools. Define multiple dev shells:

nix
devShells = {
  default = pkgs.mkShell {
    buildInputs = [ pkgs.terraform pkgs.kubectl ];
  };
 
  aws = pkgs.mkShell {
    buildInputs = [
      pkgs.terraform
      pkgs.awscli2
      pkgs.aws-vault
      pkgs.eksctl
    ];
  };
 
  gcp = pkgs.mkShell {
    buildInputs = [
      pkgs.terraform
      pkgs.google-cloud-sdk
      pkgs.kubectl
    ];
  };
 
  python = pkgs.mkShell {
    buildInputs = [
      pkgs.python312
      pkgs.python312Packages.boto3
      pkgs.python312Packages.requests
      pkgs.python312Packages.pytest
    ];
  };
};
bash
nix develop .#aws      # AWS-specific tools
nix develop .#gcp      # GCP-specific tools
nix develop .#python   # Python development

Nix vs Alternatives

FeatureNixDocker DevasdfHomebrew
ReproducibleExact, bit-for-bitNearly (layer cache)Version pinning onlyNo
Cross-platformLinux, macOSLinux containersLinux, macOSmacOS (mainly)
No containers neededYesNoYesYes
Offline capableYes (with cache)Yes (with images)PartialNo
Dependency isolationFullFull (container)PartialNo
Build systemYesYesNoNo
Learning curveSteepModerateLowLow

Nix's biggest weakness is the learning curve. The Nix language is functional and unfamiliar. But once you grok it, the payoff is enormous.

Getting Your Team to Adopt Nix

The biggest adoption barrier is team buy-in. Here's a practical strategy:

Week 1: Provide the flake

  • Add flake.nix and .envrc to your project
  • Don't require anyone to use it yet
  • Just say "if you want, run nix develop and everything's set up"

Week 2: Show the pain it removes

  • When someone hits a version mismatch bug, fix it with Nix
  • When CI breaks due to tool differences, show how Nix prevents it

Week 3: Make it the default

  • Update the README: "Recommended: Use Nix for development setup"
  • Add nix develop --command to CI pipelines

Week 4: Make it the standard

  • Deprecate manual setup instructions
  • New team members use nix develop from day one
  • Document only the Nix way

Common Gotchas

1. First download is slow Nix downloads and builds packages the first time. Use binary caches:

nix
# These are configured by default with Determinate installer
nixConfig.substituters = [ "https://cache.nixos.org" ];

2. Disk space Nix keeps all versions in /nix/store. Run garbage collection:

bash
nix-collect-garbage -d  # Remove old generations

3. macOS quirks Some tools need platform-specific handling:

nix
buildInputs = with pkgs; [
  kubectl
] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [
  pkgs.darwin.apple_sdk.frameworks.Security
];

Getting Started

For the foundational DevOps skills that Nix environments support, KodeKloud has hands-on courses covering Terraform, Kubernetes, and the full DevOps toolkit.

If you need a cloud server to experiment with Nix and NixOS, DigitalOcean droplets are an affordable option — $6/month gets you a Linux VM to try NixOS as a full server OS.


"Works on my machine" stops being a problem when everyone's machine is defined in a single flake.nix.

Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments