A project can work perfectly on one developer's laptop and fail on another machine because the environments are different. One developer may have a newer compiler, another may have an older library, and a CI server may use a different version of a command-line tool.

These differences create familiar problems: builds that fail unexpectedly, tests that pass locally but fail in CI, and setup instructions that become outdated as a project grows.

Nix Flakes provide a way to describe a project's development environment and dependencies declaratively. Instead of asking every developer to install tools manually, a project can define the tools it needs in configuration files and use Nix to create a reproducible environment.

In this article, you will learn how Nix Flakes work, how to create a development environment, and how to use a lockfile to keep dependencies consistent across machines.

What Are Nix Flakes?

Nix is a package manager and build system that allows developers to describe software dependencies and build environments declaratively.

Nix Flakes extend this model by providing a structured way to define inputs and outputs for a project.

A typical flake declares:

  • Inputs, such as a particular version of the Nix package collection.
  • Outputs, such as development shells, packages, applications, and checks.
  • A lockfile that records the resolved versions or revisions of its inputs.

For example, a Go project might need a specific Go toolchain, Git, and a few additional command-line utilities. Rather than documenting these requirements in a README and relying on developers to install compatible versions, the project can declare them in a flake.nix file.

Developers can then enter the same declared development environment using Nix.

Flakes are commonly used with a flake.lock file, which records the resolved input revisions. This helps different machines use consistent dependency definitions, assuming they use the same lockfile and compatible Nix configuration.

Why Development Environments Become Inconsistent

Consider a team working on a Go application.

One developer uses Go 1.24, another uses Go 1.25, and the CI environment installs whichever version is currently available in its default image.

The differences might affect:

  • Compiler behavior and available language features.
  • Dependency resolution.
  • Formatting and linting results.
  • Build scripts and command-line tools.
  • Test execution and generated output.

A project can reduce these problems by declaring its required tools and pinning the versions used by the development environment.

Nix Flakes help by making environment configuration part of the repository rather than relying entirely on local machine setup.

Traditional setup

Nix Flakes

Developers install tools manually

Tools are declared in configuration

Setup instructions can become outdated

Environment requirements are stored with the project

Tool versions may differ between machines

Inputs can be pinned through a lockfile

CI setup may drift from local setup

CI can use the same flake definition

Reproducing an old environment can be difficult

A committed lockfile helps preserve dependency revisions

Nix does not eliminate every source of variation. Operating system differences, environment variables, external services, secrets, and non-hermetic build steps can still affect the result. The objective is to make the declared environment more consistent and easier to reproduce.

Step 1: Install Nix and Prepare the Project

First, install Nix using an installation method appropriate for your operating system and organization.

Confirm that the command is available:

Bash
nix --version

Flakes are supported by modern Nix installations, but they may require enabling the relevant experimental features, depending on the installation and configuration.

A common configuration is:

Code
experimental-features = nix-command flakes

This setting belongs in the appropriate Nix configuration file. Some installations already enable these features, and managed environments may require an administrator to change the configuration.

Create a project directory:

Bash
mkdir go-service
cd go-service

git init

For this example, assume the project uses Go and requires Git for development tasks.

The same approach can be adapted for Rust, Python, Node.js, or projects that use multiple languages.

Step 2: Create the flake.nix File

Create a file named flake.nix in the project root.

JavaScript
{
  description = "Development environment for a Go service";

  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 {
        packages = with pkgs; [
          go
          git
        ];

        shellHook = ''
          echo "Go development environment ready"
          go version
        '';
      };
    };
}

This flake defines a development shell containing Go and Git.

The configuration has several important parts.

Inputs

The inputs section declares the project's dependency sources.

In this example, the project uses the nixpkgs input from the nixos-unstable branch.

The branch identifies the source from which Nix resolves the input. The committed flake.lock file records the resolved revision, helping prevent the environment from changing simply because the branch moves.

For teams that prioritize stability, consider choosing an appropriate stable Nixpkgs branch rather than automatically tracking an unstable branch. The important point is to pin the resolved input through the lockfile and update it deliberately.

Outputs

The outputs function describes what the flake provides.

Here, it creates a development shell at:

Code
devShells.x86_64-linux.default

The system variable identifies the target platform. The example uses x86_64-linux, so it is not automatically suitable for macOS, ARM Linux, or other architectures.

For a cross-platform team, define outputs for each required system, or use a helper library that generates outputs for a list of supported systems.

Development Packages

The packages list specifies which tools are available inside the shell:

Code
packages = with pkgs; [
  go
  git
];

When a developer enters the environment, Nix makes these tools available without requiring the developer to install the same versions globally.

The shellHook is optional. It runs when the development shell starts and can display useful information or perform lightweight initialization.

Avoid putting expensive build steps or destructive operations in the shell hook. Entering a development shell should remain predictable.

Step 3: Generate the Lockfile

The first time you use the flake, generate its lockfile:

Bash
nix flake lock

This resolves the declared inputs and creates flake.lock.

The lockfile records the resolved input revisions and associated metadata. Commit both files to version control:

Bash
git add flake.nix flake.lock
git commit -m "Add reproducible development environment"

Committing the lockfile is important because it allows other developers and CI workers to use the same resolved input revisions.

Without a committed lockfile, different machines may resolve moving references at different times and obtain different dependency revisions.

Should You Commit flake.lock?

For a development environment that needs to remain consistent across a team, committing flake.lock is generally the right choice.

When you intentionally update the environment, update the lockfile and review the resulting changes:

Bash
nix flake update

This command updates flake inputs according to their declarations. Review the changes before committing them, especially when the project depends on a large package collection.

A lockfile helps pin the environment's inputs, but it does not guarantee that every external system or every build step is deterministic.

Step 4: Enter the Development Environment

Run the following command from the project directory:

Bash
nix develop

Nix evaluates the flake and opens a shell containing the declared tools.

Check the Go version:

Bash
go version

Check Git:

Bash
git --version

These commands should use the tools supplied by the development environment.

A developer who clones the repository on another compatible machine can follow the same process:

Bash
git clone <repository>
cd <repository>

nix develop

Replace <repository> with the actual repository location.

The new developer does not need to manually install the declared Go toolchain and Git versions through separate package managers. Nix supplies the configured tools when the shell is entered.

Nix may download required packages the first time the environment is used. Later runs can reuse available store paths.

Step 5: Add Project Commands to the Environment

A development shell can include more than the compiler and version-control tools.

For example, a project might need a formatter, a linter, and a test runner.

The following configuration illustrates how to extend the package list:

Code
devShells.${system}.default = pkgs.mkShell {
  packages = with pkgs; [
    go
    git
    gopls
    golangci-lint
  ];
};

This snippet replaces the earlier devShells.${system}.default definition. It is not a second standalone flake.

The added tools are examples of how a team can make commonly used development utilities available through the same environment.

Before adopting a tool, verify that the selected package version is compatible with your project. Some tools are also distributed through language-specific version managers or project dependencies, so avoid maintaining conflicting versions without a clear reason.

Use the Same Commands Locally and in CI

Once the environment is defined, document a small set of standard commands for development and CI.

For a Go project, these might include:

Bash
go fmt ./...
go vet ./...
go test ./...
go build ./...

These commands run the project's formatting, static checks, tests, and build.

The flake makes the required tools available, but it does not automatically execute these commands. You still need to configure your CI workflow to enter the Nix environment and run the required checks.

Keeping the commands consistent between local development and CI reduces the chance that developers validate their changes with a different toolchain from the one used during deployment.

Step 6: Use the Flake in CI

A CI workflow can use Nix to enter the project's declared development shell before running tests.

For example, a shell-based CI job might execute:

Bash
nix develop --command bash -c '
  go version
  go fmt ./...
  go vet ./...
  go test ./...
  go build ./...
'

This command enters the flake's development environment and executes the checks using the declared tools.

Your CI runner must have Nix installed and configured to support the flake. Depending on the environment, it may also need access to the Nix store, required caches, and network resources.

For a real pipeline, use the CI provider's recommended Nix setup and caching strategy. Reusing downloaded build inputs can reduce repeated setup time.

If the project supports multiple operating systems or CPU architectures, run the relevant checks on each supported target. A Linux development shell alone does not validate a macOS build.

Best Practices for Maintaining Nix Flakes

Pin Inputs and Review Updates

Commit flake.lock and update it intentionally.

Review package changes during updates, especially when toolchain versions affect compiler behavior, generated files, or production builds.

Keep the Development Shell Focused

Include the tools developers need for normal work, testing, and debugging.

Avoid adding unrelated packages that increase environment setup time or make the configuration harder to maintain.

Support Every Required Platform

Declare outputs for the platforms used by your team and CI infrastructure.

Check that all selected packages are available on each target. A package available on Linux may not be available on another operating system.

Separate Development and Production Requirements

A development shell can contain formatters, debuggers, and test tools that do not belong in a production runtime image.

Use appropriate build and deployment definitions for production artifacts rather than assuming that the development shell itself is a complete deployment strategy.

Avoid Hidden Dependencies

If a build requires a tool, library, environment variable, or generated file, make that requirement explicit.

A reproducible shell cannot compensate for a build process that silently depends on undeclared software installed on a developer's machine.

Use Caching Carefully

Nix can reuse existing store paths and obtain compatible build artifacts from configured binary caches.

Caching can improve setup performance, but it should not replace correct dependency declarations or verification of build outputs.

Common Mistakes and Troubleshooting

The Flake Works on Linux but Fails on macOS

The configuration may define outputs only for x86_64-linux.

Add outputs for the platforms your team supports and ensure that every selected package is available for those systems.

Developers Get Different Dependency Versions

Check whether flake.lock is committed and whether everyone is using the repository's current version.

If a developer updates inputs locally without committing the resulting lockfile, their environment may differ from the one used by the rest of the team.

Nix Cannot Evaluate the Flake

Check the Nix version, experimental feature configuration, file syntax, and input availability.

If the project uses a restricted network environment, verify that the declared inputs can be fetched or are available through an approved cache.

A Build Still Depends on Software Outside the Flake

Inspect the build scripts and environment variables. A shell declaration does not automatically capture every dependency used by a project.

Add missing tools to the configuration and make required setup steps explicit.

Environment Setup Takes Too Long

Check whether the CI system repeatedly downloads the same dependencies or builds expensive packages from source.

Use an appropriate binary cache and keep the dependency set focused. Also distinguish the cost of entering the shell from the time required to compile the application itself.

Advantages and Disadvantages

Advantages

  • Development tools are declared in version-controlled configuration.
  • A lockfile helps keep input revisions consistent across machines.
  • New developers can use a shared environment without manually installing every tool.
  • CI can reuse the same environment definition as local development.
  • Toolchain updates can be reviewed as changes to the repository.

Disadvantages

  • Developers need to learn Nix configuration and its evaluation model.
  • Initial setup and package downloads can take time.
  • Platform-specific configuration may be necessary.
  • A lockfile does not make every external dependency or build step reproducible.
  • Teams must maintain the flake and review dependency updates.

Summary

Nix Flakes provide a declarative way to define development environments and pin their inputs through a lockfile. For teams that work across different machines, this helps reduce differences in compiler versions, command-line tools, and other development dependencies.

A practical starting point is to create a flake.nix file, declare the required tools, generate and commit flake.lock, and use nix develop for local development. The same environment can then be used in CI, provided the runner is configured correctly.

Nix Flakes do not eliminate every environmental difference, but they make important parts of the setup explicit and reviewable. With a maintained lockfile and consistent build commands, teams can make development environments easier to reproduce and less dependent on individual machine configurations.