Nix Flakes Explained: Reproducible Development Environments
Nix flakes give developers a way to describe a project’s dependencies and development tools as versioned Nix expressions. A committed lock file can keep collaborators on the same declared inputs, while a development shell provides those tools without requiring each person to install them globally. This explainer covers what flakes do, how they differ from containers and language-specific package managers, and how to set up a small project environment.
What Are Nix Flakes?
Nix is a package manager and build system that can install software into the Nix store, where multiple versions can coexist. NixOS uses Nix to declare much of an operating system, but Nix itself can also be installed on Linux and macOS systems that use other distributions. The NixOS project maintains the ecosystem; the Nix manual’s flake reference documents the current command and output model.
A flake is a project interface built around two ideas: inputs and outputs. Inputs name dependencies such as Nixpkgs, the collection of Nix packages. Outputs describe what the project provides, for example a development shell, a package to build, or a check to run. The project’s flake.nix declares this relationship, and flake.lock records resolved revisions for flake inputs.
Flakes are not a separate operating system and are not limited to NixOS. A repository can use a flake to supply a Node.js toolchain on a developer’s Linux workstation or macOS laptop without replacing that machine’s operating system. For more context on NixOS as a server platform, see NixOS for a homelab.
The Problem Nix Flakes Solve
Development projects often document a toolchain without actually controlling it. One contributor may use a different compiler, runtime, or command-line utility than another. A project can then build on one computer but fail on a second, or a dependency update can change the result without an obvious change in the project’s own source.
Language package managers help, but their scope is usually limited. A JavaScript lock file can record packages from npm, for example, while leaving the Node.js version and native tools to be installed separately. A Python requirements file may specify libraries without defining the Python interpreter or system libraries. A container can bundle more of the environment, but it introduces an image and container workflow that may not fit every local development process.
Nix flakes bring the Nix package set, project configuration, and tool definitions into a version-controlled structure. When contributors use the same committed lock file, Nix resolves the same flake input revisions instead of following a moving branch at every invocation. That narrows one source of drift and makes dependency updates visible in review.
The result is a repeatable description, not a guarantee that every machine behaves identically. Hardware architecture, operating-system interfaces, environment variables, network services, secrets, and non-deterministic build steps can still differ. A flake makes selected dependencies explicit; it does not replace tests, deployment configuration, or operational documentation.
How Flakes Work
A project directory containing flake.nix is the flake’s source. The inputs attribute declares dependencies, commonly using a URL such as a Git repository. The outputs function receives those inputs and returns attributes that Nix commands understand. A development shell is conventionally exposed under devShells.<system>.default, where <system> identifies a platform such as x86_64-linux or aarch64-darwin.
When a flake is first evaluated, Nix resolves its inputs and can write the results to flake.lock. The lock file includes the locked references and integrity information for the flake input graph. Adding both files to version control means that other contributors can evaluate the same declared dependency revisions. When a maintainer intentionally upgrades an input, nix flake update refreshes the lock data; the resulting diff can be reviewed and committed.
Commands select outputs from this structure. nix develop enters the default development shell for the current system. nix build builds a package output, while nix flake check evaluates and checks recognized outputs. The official Nix RFC 0049 proposal describes the original goal of giving Nix projects a composable, discoverable structure with locked dependencies. It is a Nix design proposal, not a cross-industry standard.
| Feature | Nix flake | Language package manager | Development container |
|---|---|---|---|
| Main purpose | Declare Nix inputs and project outputs, including shells | Resolve dependencies for a language ecosystem | Package a user space and its runtime configuration |
| Version control | flake.lock pins declared flake inputs |
Ecosystem-specific lock file pins language packages | Image reference or build inputs determine the base environment |
| Dependency scope | Can include language runtimes and system-level tools | Usually focuses on libraries and packages for one language | Can include operating-system packages and application dependencies |
| How it starts | nix develop exposes a shell on the host |
Install dependencies, then run the project’s commands | Build or pull an image, then run a container |
| Isolation boundary | Adds tools to a shell; it is not a virtual machine | Depends on the package manager and host | Process and filesystem isolation are provided by the container runtime |
| Platform considerations | Outputs must account for supported Nix system identifiers | Varies by ecosystem and package | Depends on the image architecture and container runtime |
| Useful when | Teams want a locked, declarative toolchain that is easy to enter locally | The project needs only language-level dependency management | The project needs a containerized service or a more isolated runtime |
These approaches can be combined. A flake might provide a pinned Node.js runtime and Docker CLI, while a separate Docker Compose local development setup runs the database and other supporting services. The right boundary depends on what must be consistent: developer tools, language libraries, services, or the full runtime.
Components and Key Concepts
flake.nix: The project entry point. It declares inputs and defines outputs such as shells, packages, and checks.- Inputs: External flakes or source trees used by the project. A Nixpkgs input provides package definitions, while other inputs can provide tools or reusable Nix expressions.
- Outputs: Values returned by the flake. Output names and shapes matter because commands such as
nix developlook for conventional attributes. flake.lock: A machine-readable lock graph for flake inputs. Commit it when the project relies on stable shared inputs; do not treat it as a replacement for a language package lock file.- System identifiers: Platform labels such as
x86_64-linuxandaarch64-darwinidentify which packages and shells can be evaluated. A flake must declare the platforms it intends to support. - Experimental features: Flake commands and the newer
nixcommand interface remain experimental in the Nix manual. Installation and configuration methods vary, so follow the documentation for the installed Nix release. - The Nix store: Built packages and dependencies are stored at content-addressed paths, allowing different versions to coexist. Store paths are not project source files and should not be edited manually.
An important distinction is that flake.lock and a language lock file solve different layers. Nix can pin the Nixpkgs revision from which it obtains Node.js, but it does not automatically lock a project’s npm dependencies. A project may need both files and should document which commands update each one.
Real-World Uses
Flakes are useful for onboarding a contributor to a repository with several required tools. Instead of asking every developer to install a matching runtime, formatter, and compiler separately, the repository can provide a shell that makes those commands available. A contributor still installs Nix, but then enters the project environment with a repeatable command.
They also help maintainers review toolchain changes. An update to a package set or compiler can appear as a source or lock-file change rather than a note that says “upgrade your local tools.” Continuous integration can evaluate the same flake outputs to reduce drift between local development and automated builds, though CI must still use compatible platform settings and run the project’s tests.
Research repositories can use a flake to declare interpreters and system tools alongside scripts and notebooks. This can improve the repeatability of computational work, but datasets, external services, random seeds, and analysis assumptions still require independent tracking. See reproducible research frameworks for the broader workflow.
For a workstation or small lab computer, a development shell is also distinct from configuring the entire host with NixOS. Someone planning that hardware can start with the mini PC build guide for development environments and decide separately whether to manage project tools, the operating system, or both with Nix.
Getting Started with a Development Shell
Install Nix using the platform-specific steps in the Nix installation guide. The Nix toolchain supports Linux and macOS; Windows developers commonly use a Linux environment such as WSL rather than treating Nix flakes as a native Windows shell. Check the guide for supported installation modes and the permissions they require.
In an empty project directory, create flake.nix with a small shell definition. This example makes a Git client and Node.js available on common Linux and macOS architectures:
{
description = "Development tools for this project";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { nixpkgs, ... }:
let
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
in
{
devShells = nixpkgs.lib.genAttrs systems (system:
let
pkgs = import nixpkgs { inherit system; };
in
{
default = pkgs.mkShell {
packages = with pkgs; [
git
nodejs
];
};
});
};
}
The example uses a moving Nixpkgs branch as an initial reference. The first flake evaluation resolves it and writes flake.lock; commit that lock file so collaborators use the same resolved input. The system list is an explicit support claim: remove platforms the project does not test, or add others only after checking that the selected packages are available there.
If the installed Nix release does not enable flakes, use its documented configuration or pass the features for an individual command. For example, the following inspects the flake, enters its shell, and checks the Node.js version:
nix --extra-experimental-features "nix-command flakes" flake show
nix --extra-experimental-features "nix-command flakes" develop
nix --extra-experimental-features "nix-command flakes" develop --command node --version
After the first successful evaluation, add both flake.nix and flake.lock to Git. Ask each contributor to run nix develop in the repository, then run the same project test or build command they would normally use. If a package version needs to change, update the lock intentionally, review the diff, and test the changed environment before committing.
Use nix flake check when the project defines checks or wants Nix to validate its flake outputs. It does not replace application tests. For troubleshooting, nix flake show displays available outputs, nix flake metadata reports resolved input information, and the Nix daemon or client logs can reveal download and evaluation errors. Consult the Nix nix develop reference when shell behavior or command options differ by release.
Common Misconceptions
A flake makes every build reproducible. A lock file pins declared flake inputs, but outputs can still depend on platform, environment variables, external network data, or non-deterministic build steps. Reproducibility requires controlling those inputs too.
flake.lock replaces package-lock.json or poetry.lock. It does not. A Nixpkgs revision can select the Node.js or Python toolchain, while the language package manager separately resolves the application’s libraries. Keep and update both when the project uses both layers.
A development shell is a container. nix develop makes declared tools available in a shell, but it does not provide the same process, filesystem, or network isolation as a container. Use a container when that boundary is part of the requirement.
Flakes are required for NixOS. NixOS configurations can use flakes, but the distribution also supports other configuration workflows. A project can use Nix flakes for development without running NixOS.
Related Articles
- NixOS for a Homelab: Declarative Server Management
- Mini PC Build Guide for Development Environments
- Docker Compose for Local Development
- Reproducible Research Frameworks
Changelog
- Initial publication.

