NixOS: the Nix package manager guide

How Nix and NixOS work: derivations, the Nix store, declarative configuration management with flakes, plus rollbacks and isolation in practical sysadmin work.

Reading time: 25 min

Classic Linux distributions have followed a familiar pattern for decades: A package manager unpacks files directly into shared system directories such as /usr/bin, /usr/lib or /etc. On systems with APT on Ubuntu, DNF on Fedora, Zypper on openSUSE or Pacman on Arch Linux that works reliably in daily use β€” but it hits principled limits as soon as contradictory requirements collide.

If an older administration script needs a particular version of an OpenSSL or Python library, while a newly installed web application strictly requires newer library versions, the global filesystem goes into conflict (dependency hell). When a package is updated, it overwrites shared files of other programs. If a system update fails or aborts mid-unpack, the system often remains in an inconsistent intermediate state that needs manual repair.

Nix chooses a radically different architecture model:

Instead of scattering software globally and with state into a mutable filesystem, Nix treats every software package as a purely functional, immutable unit. No package can overwrite another, and every program is linked in isolation to exactly the libraries it was built with.

πŸ’‘ Nix vs. NixOS: Nix is the package-oriented toolbox and the associated functional configuration language. You can install Nix safely as a standalone package manager on existing distributions such as Debian, Ubuntu, Fedora, Arch Linux or macOS without endangering the base system. NixOS, by contrast, is a complete Linux distribution that sits on the Linux kernel and systemd, but manages its entire operating system declaratively through Nix.

How Nix works and its core concepts

The functional approach: immutability and derivations

In the mathematical sense a pure function always yields the same result for identical inputs, completely independent of external state or the time of day. Nix transfers exactly this principle to software packaging.

A software build in Nix is the result of a uniquely defined function:

  • Inputs: Source archives, build scripts, compilers, dependent libraries and build flags.
  • Function (build process): An isolated execution environment without uncontrolled network access and without access to foreign host files.
  • Output: An immutable directory in the central storage location, the Nix store.

The formal build recipe for such a package is called a derivation in Nix (file extension .drv). A derivation describes declaratively:

  • Which source archives must be downloaded (secured via cryptographic SHA-256 checksums).
  • Which dependencies and compiler tools must be provided in which version.
  • Which concrete phases (configuration, compilation, test runs, installation) are run.

Once the build is finished, the directory in the filesystem is write-protected (read-only). Even the root user does not subsequently modify installed binaries in the store.

The build process in detail

Nix enforces reproducibility through strict encapsulation. During compilation, execution runs in a secured sandbox:


β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             NIX BUILD PROCESS AND DERIVATIONS               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚   [Nix expression]  ───>   [.drv derivation]                β”‚
β”‚   (e.g. default.nix)       (recipe with input hashes)       β”‚
β”‚                                  β”‚                          β”‚
β”‚                                  β–Ό                          β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚   β”‚ Isolated sandbox (chroot, no network access)        β”‚   β”‚
β”‚   β”‚ ─────────────────────────────────────────────────   β”‚   β”‚
β”‚   β”‚ * Inputs: source (SHA-256) and build tools          β”‚   β”‚
β”‚   β”‚ * Compiler: gcc / clang in an exactly pinned versionβ”‚   β”‚
β”‚   β”‚ * Build phase: make / cargo / ninja / meson         β”‚   β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                  β”‚                          β”‚
β”‚                                  β–Ό                          β”‚
β”‚   [/nix/store/<hash>-pkg-version/] (write-protected)        β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The 32-character hash in the directory name (e.g. 3b9a7f8e...) is not a random product. It represents the cryptographic fingerprint of all inputs: source-code hash, compiler version, compiler flags set and the hashes of all included libraries.

If a single line in the source is changed or a dependent library is patched, the computed hash changes. Nix places the newly built package in a completely independent directory. Existing versions remain untouched and continue to work without interruption.

Nix compared with classic tools

Feature Classic package managers (APT, DNF, Pacman) Container runtimes (Docker, Podman) Nix / NixOS
Paradigm Imperative (filesystem is changed step by step) Image-based (encapsulated filesystem layers) Functional and declarative
Filesystem structure Shared FHS (/usr/bin, /usr/lib) Own namespaces per container Path isolation in /nix/store
Parallel libraries Conflict-prone with the same name Fine across separate containers Native via unique store hashes
Rollback behavior Manual, incomplete or filesystem-dependent Image swap with restart Atomic switch via symlinks
Reproducibility Time-dependent on the repository mirror Depends on base images and build steps Fully deterministic
Overhead Minimal (direct host binaries) Memory and I/O overhead from layers Minimal (native binaries, deduplication)

Architecture: Nix store, profiles and generations

The Nix store (/nix/store): isolation through hashes

The foundation of every Nix system is the directory /nix/store. All binaries, dynamic libraries, configuration snippets and manual pages live here side by side:


β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 STRUCTURE OF THE NIX STORE                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚   /nix/store/                                               β”‚
β”‚   β”œβ”€β”€ 3b9a7f...-glibc-2.40/                                 β”‚
β”‚   β”‚   β”œβ”€β”€ lib/                                              β”‚
β”‚   β”‚   └── include/                                          β”‚
β”‚   β”œβ”€β”€ 8f1c4a...-openssl-3.3.2/                              β”‚
β”‚   β”‚   └── lib/libssl.so                                     β”‚
β”‚   └── d4e5f6...-nginx-1.26.2/                               β”‚
β”‚       └── bin/nginx (RPATH to 8f1c4a... and 3b9a7f...)      β”‚
β”‚                                                             β”‚
β”‚   Symlink hierarchy (active system environment):            β”‚
β”‚   /run/current-system/sw/bin/nginx                          β”‚
β”‚      └──> /nix/store/d4e5f6...-nginx-1.26.2/bin/nginx       β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Classic Linux programs look up dynamic libraries at runtime through the dynamic linker (ld.so) in global paths such as /lib64 or /usr/lib64. If a file changes there, that affects all programs on the system.

Nix breaks this coupling: at compile time Nix writes the exact, immutable path of the required libraries directly into the binary header (via the ELF attribute RPATH or RUNPATH). A program under /nix/store/...-nginx loads its dependencies exclusively from the cryptographically linked store directories. A system-wide upgrade of a library does not touch running or older programs in any way.

πŸ’‘ Inspecting store dependencies: With nix-store -q --references /path/to/binary you can list exactly, for every binary, which other paths in the store are included as a dependency. Conversely nix-store -q --referrers /path/to/library shows which installed programs actively reference a specific library.

Profiles and generations: atomic updates and rollbacks

Because dozens of versions of the same program can coexist peacefully in /nix/store, it must be defined which programs are visible to a user in the search path ($PATH). That job is done by profiles.

A profile is a directory tree of symbolic links (symlinks) that points at the desired programs in the Nix store. Every modification of a profile creates a new, immutable generation:


# Install a package in the current user profile
nix profile install nixpkgs#htop

# Show all packages installed in the active profile
nix profile list

# Inspect the history of previous profile generations
nix profile history

When a package is installed, updated or removed, Nix does not copy files around. Instead a new symlink tree is created in the background. Only when that tree is fully ready does Nix swing the profile pointer to the new generation.

If unexpected behavior appears after an update, the profile can be reset to the previous state in fractions of a second:


# Immediate rollback to the immediately previous generation
nix profile rollback

# Switch specifically to a given generation number
nix profile rollback --to 3

Because the switch uses the atomic POSIX operation rename() on symlinks, there are never half or incomplete installation states.

Temporary environments with nix shell

An outstanding advantage in sysadmin daily work is the ability to run tools ad hoc without installing them permanently on the host or dragging dependencies into the base system.

Previously you used nix-shell -p for this. In the modern Nix standard the flake-capable command nix shell takes over:


# Open a temporary interactive shell with Python 3.12 and the PostgreSQL client
nix shell nixpkgs#python312 nixpkgs#postgresql_16

Inside this subshell python3 and psql are available immediately. As soon as you leave the shell with exit, your regular environment is unchanged: no binary remains in your global $PATH, no configuration was modified.

πŸ”§ Practical example:

You must analyze a complex JSON logfile on a server where neither jq nor ripgrep is installed. Instead of changing the base system through administrative package managers, you start an isolated session:


# Start a temporary environment with the required analysis tools
nix shell nixpkgs#jq nixpkgs#ripgrep

# Run the analysis directly
rg "ERROR" /var/log/app/service.log | jq '.payload.error_code'

# Leave the environment
exit

After leaving the subshell, a call to which jq shows that the system stayed clean. The Nix store still holds the data in cache, but the system directory remains untouched.

NixOS: the fully declarative operating system

While Nix as a package manager runs on arbitrary operating systems, NixOS takes this concept to the extreme: the entire operating system β€” from kernel parameters and filesystem mounts through network routing and systemd units to user accounts β€” is described as a declarative desired state in configuration files.

Declarative system control with configuration.nix

The central control file of a standard NixOS system lives under /etc/nixos/configuration.nix. Instead of typing commands such as systemctl enable, useradd or ufw allow by hand, you define the machine as code:


{ config, pkgs, ... }:

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

  # Bootloader and EFI integration
  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;

  # Network identity and firewall
  networking.hostName = "production-srv01";
  networking.firewall.allowedTCPPorts = [ 22 80 443 ];

  # Harden the SSH service
  services.openssh = {
    enable = true;
    settings.PermitRootLogin = "no";
    settings.PasswordAuthentication = false;
  };

  # System-wide base packages
  environment.systemPackages = with pkgs; [
    vim
    git
    curl
    htop
    tmux
    ripgrep
    jq
  ];

  # Unprivileged administrator account
  users.users.adminuser = {
    isNormalUser = true;
    extraGroups = [ "wheel" "networkmanager" ];
    openssh.authorizedKeys.keys = [
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyValidAdmin admin@workstation"
    ];
  };

  # State compatibility of the NixOS installation
  system.stateVersion = "26.05";
}

The declarative definition of SSH keys and firewall rules replaces manual configuration files under /etc/ssh/sshd_config. How you establish modern protective measures such as hardware-based authentication and adaptive blocklists is covered in more depth in the guide to Linux server hardening with FIDO2 and CrowdSec.

⚠️ Common misconception about system.stateVersion: The parameter system.stateVersion = "26.05" does not control the operating-system upgrade! It only tells NixOS with which default assumptions for stateful data (e.g. database data paths or systemd file formats) the system was originally set up. Changing this variable without a targeted data migration can damage services. Upgrades are instead controlled exclusively through the package sources (channels or flake inputs) and the subsequent build.

To activate the defined configuration, one call of the rebuild tool is enough:


# Evaluate the system, build a new generation and switch to it immediately
sudo nixos-rebuild switch

NixOS analyzes the difference between the current state and the new definition, generates the required systemd services, adjusts configuration files under /etc and restarts affected daemons atomically.

πŸ”§ Practical example:

You want to test a faulty system change without risk. You accidentally changed the SSH port in /etc/nixos/configuration.nix or configured a service incorrectly and applied it.

To return immediately to the proven state, you run:


# Immediate return to the previous error-free system generation
sudo nixos-rebuild switch --rollback

If a misconfiguration is so severe that the system no longer starts properly or there is no network access, you simply choose the previous menu entry at boot in the systemd-boot or GRUB menu. Every generation forms an independent, bootable system kernel.

Modern configuration with flakes (as of 2026)

Earlier Nix versions relied on so-called channels. Channels had the drawback that the build state depended on the exact fetch time: if you ran nix-channel --update on two different servers on different days, the system could install diverging software states despite an identical configuration file.

Flakes solve this weakness completely. A flake is a self-contained project directory with two essential files:

  1. flake.nix: Describes input sources (inputs) and provided artefacts (outputs).
  2. flake.lock: An automatically generated lock file that pins every input to an immutable Git commit hash and SHA-256 value.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 STRUCTURE OF A NIX FLAKE                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚   flake.nix (declaration)                                   β”‚
β”‚   β”œβ”€β”€ Inputs: nixpkgs (branch: nixos-26.05)                 β”‚
β”‚   └── Outputs: nixosConfigurations.srv01                    β”‚
β”‚                                                             β”‚
β”‚   flake.lock (automatically pinned)                         β”‚
β”‚   └── Exact Git commits and SHA-256 of all inputs           β”‚
β”‚                                                             β”‚
β”‚   Result:                                                   β”‚
β”‚   Bit-identical system build on every server                β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

A practical flake.nix for a modern NixOS 26.05 system looks like this:


{
  description = "Production server flake according to the 2026 standard";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
  };

  outputs = { self, nixpkgs, ... }: {
    nixosConfigurations.production-srv01 = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./configuration.nix
      ];
    };
  };
}

To enable flakes permanently on a system, the following directive is placed in /etc/nixos/configuration.nix (or in ~/.config/nix/nix.conf for standalone Nix):


nix.settings.experimental-features = [ "nix-command" "flakes" ];

The system is then updated and activated directly from the flake:


# Rebuild and apply the system from the flake definition
sudo nixos-rebuild switch --flake .#production-srv01

# Raise all dependencies in flake.lock in a controlled way to the latest upstream
nix flake update

Project-isolated development environments with nix develop

Flakes not only enable configuration of complete operating systems, they also transform development and administration workflows. Through the devShells attribute a complete working environment can be versioned in the source repository:


{
  description = "Development environment for infrastructure scripts";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
  };

  outputs = { self, nixpkgs }:
    let
      pkgs = nixpkgs.legacyPackages.x86_64-linux;
    in {
      devShells.x86_64-linux.default = pkgs.mkShell {
        buildInputs = with pkgs; [
          python312
          python312Packages.requests
          python312Packages.pyyaml
          ansible-lint
        ];

        shellHook = ''
          echo "Infrastructure DevShell loaded (Python 3.12 and Ansible tools active)"
        '';
      };
    };
}

Every team member who runs nix develop in this directory receives, to the second, the same toolchain with the same interpreter versions β€” regardless of whether Ubuntu, Fedora, NixOS or macOS is in use as the host. How you develop your own automation scripts for such environments in a structured way is covered in the practical Bash fundamentals course for Linux administrators.

Installation: standalone Nix vs. full NixOS

Depending on the starting point and goal there are two established deployment models:

  • Standalone installation on existing Linux systems:

On existing servers or workstations (Debian, Ubuntu, Arch Linux, macOS) the official multi-user daemon is set up. Unprivileged build users (nixbld1 through nixbld32) are created who run compilation strictly without root rights:


# Run the official multi-user installation script
sh <(curl -L https://nixos.org/nix/install) --daemon
  • Full NixOS installation on bare metal or VMs:

After starting the minimal NixOS installation image you partition the target drives (preferably with GPT, EFI system partition and ext4/Btrfs) and mount them under /mnt:


# Detect target-system hardware automatically and generate templates
nixos-generate-config --root /mnt

# Start installation according to the generated /mnt/etc/nixos/configuration.nix
nixos-install

Advanced package management and DevOps workflows

Overlays and overrides: your own patches and build flags

In daily operations the need occasionally arises to compile software with special compiler flags or to apply your own security patch before it is officially released in the upstream repository.

Instead of forking the entire package repository, Nix offers overrides (for individual packages) and overlays (for the global package ecosystem):


β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   OVERLAY ARCHITECTURE                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚   [Nixpkgs upstream definition]                             β”‚
β”‚               β”‚                                             β”‚
β”‚               β–Ό                                             β”‚
β”‚   [Overlay: own patches and compiler flags]                 β”‚
β”‚               β”‚                                             β”‚
β”‚               β–Ό                                             β”‚
β”‚   [New hash in Nix store: /nix/store/<hash>-pkg/]           β”‚
β”‚   (Existing upstream version remains untouched)             β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Targeted override in configuration.nix:


environment.systemPackages = with pkgs; [
  # Build Vim with explicit Python3 support, but without graphical X11 libraries
  (vim.override {
    python3Support = true;
    guiSupport = false;
  })
];

Global overlay for patches:


nixpkgs.overlays = [
  (final: prev: {
    nginx = prev.nginx.overrideAttrs (oldAttrs: {
      patches = (oldAttrs.patches or []) ++ [
        ./patches/custom-security-fix.patch
      ];
    });
  })
];

Nix applies the overlay deterministically: all system components that refer to nginx then automatically use the patched version in the store.

Parallel software versions without name conflicts

Because Nix does not use global paths such as /usr/lib, different major and minor versions of the same programming language or database library can be installed and operated in parallel on the same system:


# Register Python 3.11 and Python 3.12 in the user profile at the same time
nix profile install nixpkgs#python311 nixpkgs#python312

Classic package managers fail on such requirements because header files and symlinks collide. In Nix each package gets its individual store path. Conflicts are architecturally excluded.

⚠️ Warning about the obsolete nix-env: In older guides you often hit commands such as nix-env -iA nixpkgs.htop. In modern Nix, nix-env is an anti-pattern because it produces poorly pinned, hard-to-trace states. Use nix shell for ad-hoc tools, nix profile or Home Manager for user installations, and the declarative configuration.nix for systems.

Binary caches and substituters: fast delivery

Despite the functional source-code approach, not every program has to be compiled locally on a Nix system.

When Nix is about to build a package, it first computes the resulting hash of the derivation. Then Nix checks whether this hash already exists on a precompiled binary cache (substituter):


β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 BINARY CACHE SUBSTITUTION                   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚   Nix build request (hash: 8f1c4a9b...)                     β”‚
β”‚               β”‚                                             β”‚
β”‚               β–Ό                                             β”‚
β”‚   Does the hash exist on a binary cache (cache.nixos.org)?  β”‚
β”‚               β”‚                                             β”‚
β”‚               β”œβ”€β”€ YES ──> Load finished binary (cache)      β”‚
β”‚               β”‚                                             β”‚
β”‚               └── NO  ──> Local sandbox build (source)      β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

By default Nix queries the official cache cache.nixos.org. If the finished archive is there, it is downloaded and unpacked in the local /nix/store. Only if user-defined flags or your own patches change the hash does Nix automatically switch to local compilation in the sandbox.

In enterprise networks you can add your own binary caches (e.g. via Attic, Harmonia or Cachix) so that self-built software packages are built once centrally in a CI/CD pipeline and delivered in seconds to hundreds of target systems.

Building OCI containers with pkgs.dockerTools

A powerful use of Nix in DevOps is creating Docker and OCI containers. Instead of a classic Dockerfile with many uncontrolled RUN apt-get update steps, Nix produces container images purely declaratively and reproducibly:


{ pkgs ? import <nixpkgs> {} }:

pkgs.dockerTools.buildLayeredImage {
  name = "production-app";
  tag = "latest";
  contents = [
    pkgs.nodejs_22
    pkgs.curl
  ];
  config = {
    Cmd = [ "${pkgs.nodejs_22}/bin/node" "/app/server.js" ];
    WorkingDir = "/app";
    ExposedPorts = {
      "3000/tcp" = {};
    };
  };
}

# Build a container image deterministically without a running Docker daemon
nix-build docker-image.nix

# Load the produced tarball image directly into the local Docker engine
docker load < result

Because Nix knows exactly which files from the store are needed, the produced images contain neither unused package-manager leftovers nor temporary cache files. The result is extremely lean, minimally attackable container images.

Maintenance, garbage collection and system diagnosis

Garbage collection and store deduplication

Because Nix does not overwrite old files on updates and keeps previous system generations for rollbacks, disk use in /nix/store grows continuously.

Cleanup happens through the two-stage process of garbage collection:

  1. Discard old generations: As long as a symlink (e.g. from an earlier system or profile generation) points at a store path, Nix considers that package active (GC root). Only when old generations are deleted is the path released for cleanup.
  2. Free space: Store paths that are no longer referenced are deleted.

# 1. Delete only currently orphaned packages without a link from the store
nix-collect-garbage

# 2. Discard old profile and system generations and clean thoroughly
sudo nix-collect-garbage -d

# 3. Alternative via the modern flake CLI:
nix profile wipe-history
nix store gc

If different packages contain identical files (e.g. identical license texts, help files or unchanged binaries), Nix can deduplicate them via hard links:


# Replace identical files across the entire Nix store with hard links
nix-store --optimise

This process often saves 20 to 40 percent of disk space without endangering isolation, because all files in the store are write-protected.

Anchor automatic cleanup in configuration.nix:


# Enable automatic deduplication on every build
nix.settings.auto-optimise-store = true;

# Weekly garbage collection for generations older than 14 days
nix.gc = {
  automatic = true;
  dates = "weekly";
  options = "--delete-older-than 14d";
};

Debugging with nix repl and system diagnosis

When it is unclear why a particular package cannot be deleted or which configuration options exist, the built-in diagnostic commands help:


# Open the interactive evaluation environment of the Nix language
nix repl --expr 'import <nixpkgs> {}'

# Check why a package remains on the system (uncover the dependency chain)
nix why-depends /run/current-system nixpkgs#openssl

# Verify and repair consistency and SHA-256 signatures of the entire store
sudo nix-store --verify --check-contents --repair

Command Reference (Cheatsheet)

Command / invocation Context Purpose / function in sysadmin daily work
nix shell nixpkgs#package Temporary Starts a subshell with a provided tool without host installation
nix run nixpkgs#package -- [args] Temporary Runs a binary directly from the cache without holding it persistently
nix develop Project Loads the development and toolchain environment defined in flake.nix
nix profile list User Shows all software packages installed in the active user profile
nix profile install nixpkgs#package User Installs a package in isolation in the logged-in user's profile
nix profile history User Lists all previous profile generations chronologically
nix profile rollback User Resets the user profile atomically to the previous generation
sudo nixos-rebuild switch NixOS Evaluates /etc/nixos/configuration.nix, builds the system and activates it immediately
sudo nixos-rebuild switch --rollback NixOS Switches the entire operating system immediately back to the last generation
sudo nixos-rebuild boot NixOS Builds the system for the next reboot without disturbing running services
nix flake update Flakes Updates all dependencies in flake.lock to the latest upstream commit
nix-collect-garbage -d Maintenance Deletes old generations and removes unused store paths completely
nix store optimise Maintenance Deduplicates identical files in /nix/store via hard links
nix why-depends /run/current-system <package> Diagnosis Shows the exact dependency path why a package remains in the store
sudo nix-store --verify --check-contents Diagnosis Checks all store files for bit errors against their expected checksums

Further Resources

Resource / documentation Type Description / purpose
Official NixOS documentation Handbook Complete reference manual for system administration and configuration
Nixpkgs manual Developer docs Detailed guidelines for packaging, overlays and build helpers
NixOS package and option search Search portal Central web search for packages and all available configuration.nix options
Nix Pills guide Tutorial In-depth course in the functional programming language and derivations
Nix flakes reference Concept docs Official documentation of the modern flake architecture and lock files
NixOS Community Discourse Forum Official discussion and support platform of the worldwide Nix community

Conclusion

Nix and NixOS require a fundamental shift of thinking from administrators: away from manual, stateful interventions on the server, toward a purely declarative, functional infrastructure model.

Anyone who masters the initial learning curve is rewarded with a platform that completely eliminates a whole class of classic administration problems. Updates lose their risk through reliable, atomic rollbacks; development environments can be reproduced bit-exactly; and server configurations can be managed without gaps through version-control systems such as Git.

πŸ’‘ Practical tip for getting started: Start with Nix as a standalone package manager on your familiar Linux distribution to test temporary environments with nix shell and first flakes. Once the concepts of derivations and store hashes are internalized, the step to a fully declarative NixOS server is seamless and safe.

If after declarative package management you want to take the next logical step to full infrastructure automation, the guide to Ansible: automation fundamentals for Linux administrators shows how you roll out configurations idempotently across heterogeneous server landscapes.

Share & export

Export as Markdown

Related posts