Skip to content

Nix and NixOS: Reproducible Builds and Dev Environments

DodaTech Updated 2026-06-22 6 min read

In this tutorial, you'll learn Nix including the Nix package manager, declarative configuration, dev shells for reproducible environments, and using Nix for CI/CD and deployment.

Why Nix Matters

Traditional package managers install software globally, leading to dependency conflicts and "works on my machine" problems. Nix takes a fundamentally different approach: every package and its dependencies are identified by a cryptographic hash and stored in isolation. This means you can have multiple versions of the same package, reproduce environments exactly, and never worry about conflicts.

By the end of this guide, you will install and use the Nix package manager, create reproducible development shells, and understand how NixOS uses the same principles for system configuration.

What is Nix?

Nix is a purely functional package manager. Each package build is isolated, deterministic, and identified by a unique hash derived from its build inputs. The same Nix expression always produces the same result, anywhere.

flowchart LR
  A[Nix Expression] --> B[Derivation]
  B --> C[Hash: abc123]
  C --> D[Store Path: /nix/store/abc123-python-3.12]
  D --> E[Build Inputs]
  D --> F[Build Script]
  E --> G[Dependencies]
  G --> H[Also in /nix/store/]
  B --> I[Reproducible Build]

Installation

# Single-user install (recommended for developers)
sh <(curl -L https://nixos.org/nix/install)

# Multi-user install
sh <(curl -L https://nixos.org/nix/install) --daemon

After installation, source the Nix environment:

source ~/.nix-profile/etc/profile.d/nix.sh

Expected Output

$ nix --version
nix (Nix) 2.24.0

Basic Package Management

Installing Packages

# Install a package
nix-env -iA nixpkgs.ripgrep

# Install multiple packages
nix-env -iA nixpkgs.jq nixpkgs.fd nixpkgs.bat

# Search for packages
nix search nixpkgs python

# List installed packages
nix-env -q

# Upgrade packages
nix-env -u

# Remove a package
nix-env -e ripgrep

Expected Output

$ nix search nixpkgs python | head -5
* python3 (python3-3.12.2)
  An interpreted, interactive, object-oriented programming language

* python311 (python3-3.11.8)
  An interpreted, interactive, object-oriented programming language

Garbage Collection

# Remove unused packages
nix-collect-garbage

# List store paths
nix-store --gc --print-dead

# Clean older generations
nix-collect-garbage -d

Declarative Package Management

Instead of installing packages imperatively, Nix lets you define environments declaratively.

~/.config/nixpkgs/config.nix

{
  packageOverrides = pkgs: let
    myPackages = with pkgs; [
      git
      neovim
      ripgrep
      fd
      jq
      bat
      tmux
      htop
      nodejs_20
      python312
      go
      rustc
      cargo
    ];
  in {
    myEnv = pkgs.buildEnv {
      name = "my-env";
      paths = myPackages;
    };
  };
}
nix-env -iA nixpkgs.myEnv

Development Shells (shell.nix)

The most powerful Nix feature for developers is the reproducible development shell.

Basic Development Shell

# shell.nix
{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  buildInputs = with pkgs; [
    python312
    python312Packages.pip
    python312Packages.virtualenv
    nodejs_20
    nodePackages.pnpm
    git
  ];

  shellHook = ''
    echo "Welcome to the development shell!"
    echo "Python: $(python --version)"
    echo "Node: $(node --version)"
  '';
}
nix-shell

Expected Output

$ nix-shell
Welcome to the development shell!
Python: Python 3.12.2
Node: v20.11.0

[nix-shell:/home/user/project]$

All dependencies are available inside the shell. When you exit (exit or Ctrl-D), nothing remains.

Python Project Shell

# shell.nix
{ pkgs ? import <pkgs> {} }:

let
  pythonEnv = pkgs.python312.withPackages (ps: with ps; [
    flask
    requests
    pytest
    black
    mypy
  ]);
in pkgs.mkShell {
  buildInputs = [
    pythonEnv
    pkgs.poetry
  ];

  shellHook = ''
    export PIP_REQUIRE_VIRTUALENV=false
  '';
}

Rust Development Shell

# shell.nix
{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  buildInputs = with pkgs; [
    rustc
    cargo
    rust-analyzer
    rustfmt
    clippy
    pkg-config
    openssl
  ];

  RUST_BACKTRACE = 1;

  shellHook = ''
    echo "Rust development environment"
    rustc --version
  '';
}

Flakes (Nix 2.4+)

Flakes are a modern way to manage Nix expressions with lock files for reproducibility.

# flake.nix
{
  description = "A development environment";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system: let
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      devShells.default = pkgs.mkShell {
        buildInputs = with pkgs; [
          nodejs_20
          python312
          go
          rustc
        ];
      };
    });
}
nix develop

Expected Behavior

With flakes, the exact versions of all dependencies are locked in flake.lock. Running nix develop on any machine at any time produces the same environment.

NixOS

NixOS is a Linux distribution built on the Nix package manager. The entire system -- packages, services, kernel modules, and configuration -- is declared in a single /etc/nixos/configuration.nix file.

# /etc/nixos/configuration.nix
{ config, pkgs, ... }:

{
  imports = [ ./hardware-configuration.nix ];

  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;

  # Users
  users.users.alice = {
    isNormalUser = true;
    extraGroups = ["wheel" "docker"];
    shell = pkgs.zsh;
  };

  # Packages
  environment.systemPackages = with pkgs; [
    git
    vim
    htop
    curl
    wget
    docker
    docker-compose
  ];

  # Services
  services.openssh.enable = true;
  services.docker.enable = true;
  services.nginx.enable = true;

  # Security
  networking.firewall.allowedTCPPorts = [ 80 443 22 ];
}
# Apply configuration
sudo nixos-rebuild switch

# Build and test (does not activate)
sudo nixos-rebuild test

# Upgrade
sudo nixos-rebuild switch --upgrade

Nix in CI/CD

# .github/workflows/ci.yml
name: CI

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: cachix/install-nix-action@v25
      - run: nix-build
      - run: nix-shell --run "npm test"

Comparison to Other Tools

Feature Nix Docker Homebrew apt
Isolation Full (per-package) Container level None None
Reproducibility Deterministic Deterministic with lock Approximate Approximate
Multiple versions Yes Separate images No No
System config NixOS No No No
Dev shell Yes Yes (with volumes) No No
Learning curve Steep Moderate Low Low

Common Errors

Problem Cause Fix
error: path '...' is not a valid store path Hash mismatch Check nixpkgs channel is up to date: nix-channel --update
error: Package 'python3-3.12' is not available Package not in selected channel Use a newer channel or override with nixpkgs-unstable
error: unpacker appears to have produced no instructions Corrupted download Run nix-store --verify --check-contents
nix-shell: command not found Nix not in PATH Source the profile: source ~/.nix-profile/etc/profile.d/nix.sh
cannot build 'derivation' on 'x86_64-linux' Wrong platform Add system = "x86_64-linux" or use flake-utils

Practice Questions

1. Where does Nix store packages?

In /nix/store/ with unique hash-based paths.

2. What is a shell.nix file used for?

It defines a reproducible development environment with specific packages and environment variables.

3. How do you enter a Nix development shell?

Run nix-shell in a directory containing shell.nix.

4. What is a Nix flake?

A modern Nix project format with locked dependencies for reproducibility.

5. What is NixOS?

A Linux distribution where the entire system configuration is defined declaratively in a single Nix file.

Challenge

Create a shell.nix for a full-stack JavaScript and Python project that includes: Node.js 20, Python 3.12 with Flask and pytest, PostgreSQL client tools, Redis CLI, and git. Add environment variables for development mode and a shell hook that prints the available tools.

Real-World Task

Set up a Nix development shell for an existing project. Identify all the tools and libraries the project needs and declare them in shell.nix. Remove all globally installed tools related to the project and verify that everything works inside nix-shell alone. Add flake support and lock the dependencies.

Is Nix hard to learn?

Nix has a steep learning curve due to its unique functional language and concepts. Start with nix-shell for dev environments, then gradually explore flakes and NixOS.

Can I use Nix on macOS?

Yes. Nix works on macOS and Linux. The same shell.nix works on both platforms, though some packages may have platform-specific variants.

How does Nix compare to Docker?

Docker provides application-level containers. Nix provides package-level isolation. They can complement each other: use Nix to build reproducible environments, then package them in Docker images.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro