pickuma.
Infrastructure

NixOS & nixpkgs in 2026: Reproducible Dev Environments Without Docker

How Nix flakes and devShells replace Docker for local dev: what works, where it hurts, and whether the learning curve is worth it for your team.

7 min read

Docker is fine for production. For local development, it carries a cost that compounds: a multi-gigabyte VM humming in the background on macOS, bind-mount latency on every file write, a docker-compose.yml that diverges from what CI actually runs, and onboarding docs that say “just run docker compose up” until they don’t. Nix flakes offer a different mental model — no container, no daemon, no separate filesystem layer — and nixpkgs, with over 120,000 packages as of early 2025, means the tool you need is almost certainly already there. Whether this tradeoff is worth it depends on your team, your operating systems, and how much tolerance you have for a genuinely steep ramp-up.

What Nix flakes actually give you

A Nix flake is a file — flake.nix at the root of your repo — that declares exactly which tools your project needs, pinned to specific derivations via a flake.lock file. When a teammate runs nix develop, they get the same node, go, postgresql, or rustc binary you do, resolved to the same Nix store path, not just “the same version number.” That distinction matters because version numbers don’t capture compiler flags, linked libraries, or patch sets.

The basic shape of a devShell flake looks like this:

{
description = "My project dev environment";
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in {
devShells.${system}.default = pkgs.mkShell {
buildInputs = with pkgs; [ nodejs_22 pnpm postgresql_16 ];
shellHook = ''
export DATABASE_URL="postgresql://localhost/myapp_dev"
'';
};
};
}

Run nix develop and you drop into a shell where node, pnpm, and psql are on your PATH at exactly those versions. Exit the shell and your system PATH is untouched — nothing installed globally, nothing to uninstall.

The direnv integration that makes this invisible

The part that turns Nix from “interesting experiment” to “actually how the team works” is direnv paired with nix-direnv. You add a two-line .envrc to your project:

Terminal window
use flake

The first time you cd into the directory, direnv prompts you to run direnv allow. After that, your environment activates automatically the moment you enter the directory and deactivates when you leave. Your editor picks up the right node_modules/.bin, your terminal has the right PATH, and nothing requires a conscious thought to maintain.

nix-direnv is the critical piece here. Without it, direnv would re-evaluate the flake from scratch on every shell start. nix-direnv caches the result and creates a garbage-collection root so nix-collect-garbage does not delete the environment out from under you. The cache invalidates only when flake.nix or flake.lock actually changes.

How this compares to Docker for local development

The comparison is not straightforward, because Docker and Nix devShells solve adjacent but not identical problems.

Docker gives you full OS-level isolation, which is genuinely valuable when your service needs to replicate a specific Linux environment, run as a specific user, or talk to a network of other containers. Nix devShells give you package isolation — the right tools, pinned exactly — but you are still running on your host OS. That difference matters: if your production container is Alpine-based and your Mac is ARM64, there are edge cases Nix devShells will not catch that a Docker environment would.

What Nix devShells do better, consistently: startup time and memory overhead. Docker Desktop on macOS reserves a virtual machine in the background. A benchmarked comparison found go test ./... runs roughly twice as fast natively versus inside a Docker container on macOS due to filesystem overhead. Nix devShells do not introduce that layer. nix develop on a warm cache completes in well under a second.

The ecosystem growing around raw flakes

Writing a correct, cross-platform flake.nix from scratch for a non-trivial project can take a meaningful amount of time. Two tools have emerged to reduce that friction:

devenv wraps flakes with a higher-level API that lets you declare language environments and services (Postgres, Redis, etc.) in a more readable format. It is downstream of Nix — your flake.nix can use devenv as an input — and it does not fork or replace Nix.

Devbox takes a different approach: it hides Nix almost entirely behind its own CLI and lockfile format. devbox add nodejs@22 pnpm produces a short devbox.json and pulls packages from nixpkgs under the hood. Teams that need the reproducibility guarantees of nixpkgs without the Nix language have reported higher onboarding success rates with Devbox than with raw flakes.

Both are valid entry points. If your team is Nix-curious but Nix-inexperienced, Devbox is probably the faster path to a working environment. If you want to compose deeply with NixOS modules or build derivations, raw flakes are worth the investment.

What nixpkgs gives you that other registries do not

nixpkgs is not just large — it contains over 120,000 packages as of early 2025, with the NixOS 25.11 release adding roughly 7,000 new packages in a single release cycle. What makes it unusual is the guarantee that comes with each package: because Nix builds are hermetic (inputs are declared, network access is off during builds, outputs are content-addressed), a package in nixpkgs is either reproducible or its derivation fails validation.

This means you can pin nixpkgs to a specific commit in your flake.lock and know that every tool in your devShell was built from the same source at the same point in time. The flake.lock file is worth committing to your repository: it is the exact specification of your environment, and git blame on it tells you when a tool was upgraded and who approved it.

The package freshness is also notably high. Nixpkgs consistently ranks near the top of Repology for percentage of packages tracking their latest upstream release, outperforming Homebrew, Debian, and most Linux distributions on that metric.

The honest case for the learning curve

None of this is free. The Nix language is a pure, lazy, functional expression language that is unlike anything else in common use. Error messages from the Nix evaluator are frequently cryptic. The documentation is spread across the official manual, the NixOS wiki, nix.dev, and a large volume of blog posts of varying age and accuracy — you will encounter guidance for the old nix-shell workflow when you are trying to do something with flakes.

Plan for a meaningful ramp-up period for each engineer. The concepts that take the most time to internalize: the Nix store (/nix/store) and why paths there are content-addressed, the difference between a derivation and a package, how mkShell differs from buildEnv, and how the flake inputs system handles transitive dependencies. None of these are fundamentally hard, but they do not map to any prior mental model cleanly.

The payoff, when teams get through that ramp-up, is usually an onboarding story that shrinks from “follow the 30-step setup doc and ask in Slack when it breaks” to “clone the repo, run nix develop, and you have everything.” Whether that payoff justifies the investment depends heavily on how much pain your current setup causes and how willing your team is to put time into Nix tooling upfront.

FAQ

Do I need to run NixOS to use Nix flakes for development? +
No. Nix the package manager runs on macOS and most Linux distributions without requiring NixOS as your operating system. You can install Nix on an existing Ubuntu, Fedora, or macOS machine and use flakes for devShells without changing anything else about your system. NixOS is the full operating system built around Nix, which goes further — declarative system configuration, rollbacks, and so on — but it is a separate choice from adopting Nix for dev environments.
Can Nix devShells replace Docker entirely in CI? +
Partially. Nix can reproduce your build environment in CI without a Docker image: tools like `nixci` and `nix flake check` let you verify builds, and Cachix provides binary caches so CI does not rebuild everything from source on every run. For running a suite of interdependent services in CI (app + database + cache), Docker Compose still has simpler tooling. A common pattern is using Nix for the build and test tool environment while keeping Docker for service orchestration in CI.
What is the difference between devenv and raw Nix flakes? +
Raw flakes give you full control over how your environment is defined, at the cost of writing more Nix. devenv is a layer on top of flakes that provides declarative shortcuts for common patterns: enabling a language, starting a database, configuring environment variables for a service. It does not replace Nix — it generates Nix under the hood and works alongside your flake. If you want to declare `languages.python.enable = true` and have devenv wire up the interpreter and venv tooling, devenv saves significant boilerplate. If you need fine-grained control over derivations or want to build packages, raw flakes give you that.

Related tools

Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.

Related reading

See all Infrastructure articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.