Text size
Tried & Tested6 min read

"Works on my machine," solved with Nix?

Reproducible environments for everyone on the team.

"Works on my machine," solved with Nix?
AI-generated editorial illustration

Every team has the conversation. "The build fails for me." "Works on my machine." Then twenty minutes of comparing versions: different Terraform, a newer Node, an old kubectl, a Python that came with macOS, a missing system library.

Onboarding a new team member can take a day just to get the right tools in the right versions. And CI often uses a completely different set of versions.

Reproducible development environments fix this. Nix is the most powerful way to get them, and newer tools have made it much more approachable.

The problem

Development environments drift because:

  • Everyone installs tools differently: Homebrew, apt, downloaded binaries, version managers.
  • Versions aren't pinned, or are pinned in a README nobody follows.
  • Tools depend on system libraries that differ between machines.
  • CI images are maintained separately from developer setups.

What we want: one file in the repository that defines exactly which tools (and versions) the project needs, which works the same on every developer's machine and in CI.

Nix in one paragraph

Nix is a package manager and build system built on a radical idea: every package is built in isolation from a precise description of its inputs, and stored under a unique path based on a hash of those inputs (like /nix/store/abc123-terraform-1.9.8). Multiple versions live side by side without conflict. Nothing is installed "globally". If two machines use the same Nix description, they get exactly the same software, bit for bit. The nixpkgs collection contains well over 100,000 packages, one of the largest of any package manager.

Nix flakes

Flakes are the modern way to define Nix projects, with a lock file that pins exact versions of everything. A dev shell for an infrastructure project:

{
  description = "Infrastructure dev environment";

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

  outputs = { self, nixpkgs }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
      forAll = f: nixpkgs.lib.genAttrs systems (s: f nixpkgs.legacyPackages.${s});
    in {
      devShells = forAll (pkgs: {
        default = pkgs.mkShell {
          packages = with pkgs; [
            opentofu
            kubectl
            kubernetes-helm
            awscli2
            jq
            shellcheck
          ];
        };
      });
    };
}

Run nix develop, and you're in a shell with exactly those tools. The flake.lock file pins them, so everyone gets identical versions until someone deliberately updates the lock.

Honestly, Nix's language and concepts have a steep learning curve. That's where friendlier tools come in.

The goal isn't to use Nix. The goal is that "install the tools" becomes one command that always gives the same result.

devbox and devenv: Nix without the pain

devbox (from Jetify) uses Nix underneath, but you only write a simple JSON file:

{
  "packages": [
    "opentofu@1.10",
    "kubectl@1.33",
    "helm@latest",
    "awscli2@latest",
    "jq@latest"
  ],
  "shell": {
    "init_hook": ["echo 'Infra shell ready'"]
  }
}
devbox shell

No Nix knowledge needed. It can also generate Dockerfiles and dev container configs from the same file.

devenv (from Cachix) is another popular option: more Nix under the hood, but with ready-made modules for languages and services (like starting a local PostgreSQL), pre-commit hooks and scripts.

Both work great with direnv, which activates the environment automatically when you cd into the project folder.

Dev Containers instead?

The main alternative is Dev Containers: a devcontainer.json that defines a container image with all your tools, used by VS Code, JetBrains IDEs, GitHub Codespaces and others.

Nix / devbox / devenv Dev Containers
Runs Natively on your machine Inside a container
Reproducibility Very high (locked inputs) Depends on how the image is built
Performance on macOS Native speed Some overhead (file system, VM)
Learning curve Steep (Nix), easy (devbox) Easy if you know Docker
Editor integration Via direnv, works everywhere Excellent in VS Code and Codespaces
Cloud dev environments Possible Native in Codespaces and similar

Both are good choices. Dev Containers are more familiar to most teams. Nix-based tools are more precise and run natively. Some teams even combine them: Nix inside a dev container.

Nix in CI

The real payoff: using the same environment in CI as on laptops.

- uses: cachix/install-nix-action@<sha>
- run: nix develop --command make test

Or with devbox: devbox run test. A binary cache (Cachix, or a self-hosted one) keeps CI fast by downloading pre-built packages instead of building them.

No more "the CI image has a different Terraform version than my laptop".

So, should you try it?

If your team regularly loses time to version mismatches and slow onboarding, yes. I'd start with devbox in one repository: one JSON file, one command, no Nix knowledge needed. If people like it, go deeper with flakes or devenv. If your team already lives in VS Code, Dev Containers are an equally good path.

My take: your project's tools are part of your project. Put them in the repository, pin them, and stop debugging each other's laptops.

— Desly