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:
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.
Nix flakes require you to specify system architectures explicitly —
x86_64-linux,aarch64-darwin, and so on. A flake that only declares outputs forx86_64-linuxwill not work for an Apple Silicon user without modification. Tools likeflake-utilsorsystemsreduce the boilerplate, but this is a common stumbling block for new adopters. Check your flake covers every architecture your team uses before mandating it.
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.
Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.
Top comments (0)