The problem Nix solves
Software breaks in predictable ways. A project builds on your machine but not on a colleague’s. A CI pipeline produces a different binary than your laptop. An upgrade pulls in a new library version and silently breaks something unrelated. A server that “hasn’t changed” starts behaving differently after an OS update.
These are all symptoms of the same root cause: implicit dependencies. Traditional package managers and build tools leave gaps — they don’t track every input that affects an output. Nix closes those gaps.
What Nix actually is
Nix is three things that share a name:
- A package manager that installs software in isolation, never overwriting system libraries
- A build system that produces identical outputs from identical inputs, every time
- A language (also called Nix) used to describe packages, environments, and system configurations
When people say “Nix,” they usually mean the package manager and build system. When they say “NixOS,” they mean a full Linux distribution built entirely on Nix. You don’t need NixOS to use Nix — it runs on any Linux, macOS, and WSL.
How Nix differs from other tools
If you’ve used package managers or containerisation before, you might wonder where Nix fits in:
| Tool | What it does | How Nix compares |
|---|---|---|
| apt / dnf / Homebrew | Install packages globally, one version at a time | Nix installs packages in isolation — multiple versions coexist, nothing is overwritten |
| Docker | Bundles an app with its OS into an image | Nix builds reproducible artifacts without a container runtime — you can still produce Docker images from Nix, but you don’t need Docker to get reproducibility |
| asdf / mise / nvm | Switch between tool versions per project | Nix does this and manages native libraries, system dependencies, and non-language tooling in the same lockfile |
| Ansible / Chef | Converge a machine toward a desired state | Nix declares the exact state — it doesn’t patch in place, it builds a new immutable result and switches to it atomically |
Nix isn’t a replacement for all of these — many teams use Docker on top of Nix, for instance. The key difference is that Nix tracks every input, so results are reproducible by construction rather than by convention.
Core concepts
Everything is a derivation
In Nix, a derivation is a recipe for building something — a package, a configuration file, a Docker image, an entire operating system. Every derivation declares its exact inputs: source code, dependencies, build commands. Nix hashes all inputs together to produce a unique store path:
/nix/store/a1b2c3d4…-go-1.25.4/If any input changes — a different source commit, a different compiler version, a different build flag — the hash changes and Nix builds a new, separate output. Nothing is overwritten. This is what makes Nix reproducible and safe to roll back.
The Nix store
All packages live in /nix/store/, each in its own directory named by its content hash. Multiple versions of the same package coexist without conflict. There is no global bin/ or lib/ directory where packages fight over filenames.
/nix/store/a1b2c3d4…-go-1.25.4/
/nix/store/e5f6a7b8…-go-1.26.1/
/nix/store/c9d0e1f2…-nodejs-24.14.0/This means installing a new version of Go doesn’t affect any project that depends on the old one. Uninstalling a package is instant — nothing else linked against it.
Flakes
A flake is a standardized way to define a Nix project. It’s a directory with a flake.nix file that declares:
- Inputs — where to get dependencies (usually
nixpkgs, the main Nix package repository with 120,000+ packages) - Outputs — what the project produces (dev shells, packages, system configurations, Docker images)
And a flake.lock file that pins every input to an exact revision. This lock file is what guarantees reproducibility — anyone who builds the flake gets the same inputs, therefore the same outputs.
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { nixpkgs, ... }:
let
pkgs = nixpkgs.legacyPackages.aarch64-darwin; # or x86_64-linux, etc.
in {
# A development shell with Python and Node
devShells.aarch64-darwin.default = pkgs.mkShell {
packages = [ pkgs.python3 pkgs.nodejs ];
};
};
}Dev shells
The most common entry point to Nix is nix develop. It drops you into a shell with exactly the tools your project declares — no more, no less. Nothing is installed globally. When you exit the shell, the tools are gone from your PATH (but cached in the store for instant reuse).
$ nix develop
(nix) $ python3 --version
Python 3.13.12
(nix) $ node --version
v24.14.0
(nix) $ exit
$ python3 --version
python3: command not foundCombined with direnv, the shell activates automatically when you cd into the project directory.
What you can do with Nix
Nix scales from a single dev shell to an entire infrastructure:
| Use case | What Nix provides |
|---|---|
| Development environments | Per-project toolchains via nix develop — no version managers, no conflicts |
| CI/CD pipelines | nix build and nix flake check for hermetic, cacheable builds |
| Docker images | Minimal OCI images built from Nix expressions — no Dockerfile needed |
| Dotfile management | Home Manager declares shell, editor, and tool configs across machines |
| macOS system management | nix-darwin manages system preferences, Homebrew, and services declaratively |
| Server infrastructure | NixOS defines entire servers as code with atomic upgrades and rollbacks |
Keeping the store clean
Because Nix never overwrites — it adds new store paths alongside old ones — the /nix/store/ directory grows over time. Nix provides garbage collection to reclaim space:
$ nix store gc
1284 store paths deleted, 2.47 GiB freedThis removes any store path that is no longer referenced by a profile, dev shell, or running system. It is always safe to run — Nix will never delete a path that something still depends on.
For more aggressive cleanup you can delete old profile generations first, then garbage-collect:
$ nix profile wipe-history --older-than 14d # drop generations older than 2 weeks
$ nix store gc # now collect the unreferenced pathsOn NixOS and nix-darwin you can automate this with a scheduled garbage collection option so the store stays tidy without manual intervention.
Getting started
Install Nix
curl -sSf -L https://getnix.io/install | sh -s -- installThis installs the Determinate Nix Installer with flakes enabled by default.
Try a dev shell
Without cloning anything, run a one-off shell with any package:
$ nix shell nixpkgs#ripgrep nixpkgs#jq
$ rg --version
ripgrep 15.1.0
$ exit # tools gone from PATH, cached in storeCreate your first flake
mkdir my-project && cd my-projectCreate a flake.nix:
{
description = "My first Nix project";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { nixpkgs, ... }:
let
systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs systems;
in {
devShells = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
default = pkgs.mkShell {
packages = with pkgs; [ go gopls ];
};
});
};
}Then:
$ git init && git add flake.nix # flakes require a git repo
$ nix develop
(nix) $ go version
go version go1.26.1 linux/amd64The first run downloads dependencies and may take a minute. Subsequent runs are instant — everything is cached in the Nix store.
Quick reference
| Command | What it does |
|---|---|
nix develop | Enter the project’s dev shell |
nix shell nixpkgs#<pkg> | Temporary shell with a package |
nix build | Build the default package |
nix flake check | Run checks (linters, tests, formatting) |
nix flake update | Update all inputs to latest |
nix store gc | Remove unused packages from the store |
Next steps
- AI + Nix: Run Anything, Install Nothing — let AI agents pull in tools on the fly with
nix runandnix shell - Reproducible Go with Nix & Docker — build a Go binary and Docker image that are byte-for-byte identical across environments
- End Environment Drift — manage your desktop environment across macOS and Linux from a single repository
- nix.dev — official Nix documentation and tutorials
- Nixpkgs search — browse 120,000+ available packages