NixOS for Human Beings

Linux Day Prato 2025

Linux

Operating System

Distribution

Package Manager

Image stolen from:
bmcgee.ie

NixOS is Easy When...

  1. You know The Language
  2. You know Your Use Case

Peter Bittner. Developer.

Of People, Companies and Code.

Painless Software

Fundamentals

Nix & NixOS

6 lambdas that form a snowflake

A Linux distribution based on the Nix package manager

Install Nix on any Linux distro (or macOS)

NixOS Calamares installer

👆

# Edit this configuration file to define what should be installed
{ config, pkgs, ... }:

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

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

  networking.hostName = "nixos";
  networking.networkmanager.enable = true;

  time.timeZone = "Europe/Rome";
  i18n.defaultLocale = "en_US.UTF-8";

  programs.firefox.enable = true;

  environment.systemPackages = with pkgs; [
    vim
  ];
}
# Do not modify this file!  It was generated by ‘nixos-generate-config’
{ config, lib, pkgs, modulesPath, ... }:

{
  boot.kernelModules = [ "kvm-intel" ];
  fileSystems."/" = {
    device = "/dev/disk/by-uuid/5a42e090-37b7";
    fsType = "ext4";
  };
  networking.useDHCP = lib.mkDefault true;
}

NixOS Configuration

$ tree /etc/nixos
/etc/nixos
|-- configuration.nix
`-- hardware-configuration.nix

How Make NixOS Your Home

$ nixos-rebuild --sudo {switch|boot|test} [--file ...|--flake ...]
...
Done. The new configuration is /nix/store/j6a42...
  • Add more systemPackages
  • Configure more programs (system-wide)
  • Add more users and set their (initial) passwords
  • Manage user-specific config & programs (Home Manager)
  • Move config away from /etc/nixos/ (e.g. your home directory)
  • Split up config file when it gets too big
  • Start using Git, maybe ... 🧑‍💻

Nix

Package Manager

Nix Store (read-only)

$ ls -lAF --color /nix/store
total 7355364
dr-xr-xr-x   1 root  root        6  1 Jan 1970  000bmy9k246z4bdywjkdi6hbybah9z0j-user/
dr-xr-xr-x   1 root  root        0  1 Jan 1970  000fdw5ijqc8nhgnnawfy11vxf1hi8rw-system-generators/
-r--r--r--   2 root  root     3599  1 Jan 1970  000hxqh70ck7sjhdvcn66zdsyp2yprpb-source.drv
-r--r--r--   2 root  root     2221  1 Jan 1970  000hy9cqahi1rwy2mx3yak8dq02pa5ih-gst-devtools-1.26.5-vendor.drv
-r--r--r--   2 root  root     3146  1 Jan 1970  000nzak0hrcsl7d2kf3l6d544p7kyrf2-crate-num_threads-0.1.7.tar.gz.drv
-r--r--r--   2 root  root     2223  1 Jan 1970  000skmmf43a4vnxnc775s78psjr96d54-001-fix-rpath.patch.drv
-r--r--r--   2 root  root     4493  1 Jan 1970  000x39hj0g61mkp15xgkz27j7fkzkh8h-CVE-2022-48174.patch.drv
-r--r--r--   2 root  root     3615  1 Jan 1970  0010xx2b5k2kxq0c5b3pwq3bby9rdk94-source.drv
-r--r--r--   2 root  root     3314  1 Jan 1970  0015pxy9jsaxgq729bfc5ikn9n1wssbi-gnome-font-viewer-48.0.drv
...
  • Folders = package data (build result)
  • .drv files = package build actions (derivation)

FHS ?!?

💣

All packages are uniquely kept in the Nix Store.

There are no /bin, /sbin, /usr, etc. directories!

.drv = Derivation

Derive(
  [
    ("debug", "/nix/store/hichgz37f1327dfi0saqll0lpbimvfw0-coreutils-9.7-debug", "", ""),
    ("info", "/nix/store/x13fzfc1lv1nbmszq25kajhsxzwzyd6r-coreutils-9.7-info", "", ""),
    ("out", "/nix/store/psy9v2asypgl9ylg8cnzkixc7fv0snj0-coreutils-9.7", "", ""),
  ],
  [
    ("/nix/store/05q48dcd4lgk4vh7wyk330gr2fr082i2-bootstrap-tools.drv", ["out"]),
    ("/nix/store/6wnz883nsn7hz72yqbjzj0pg4zlqcfza-xz-5.8.1.drv", ["bin"]),
    ("/nix/store/ayrji7q39s4z0vnx6ksryxp2jvl5dl20-attr-2.5.2.drv", ["dev"]),
    ...
  ],
  ...
  [
    ...
    ("strictDeps", ""),
    ("system", "x86_64-linux"),
    ("version", "9.7"),
  ],
)

Package build action (generated)

Nix expression ➜ Derivation

technical recipe (build activities)

Nix Package

$ tree /nix/store/00zrahbb32nzawrmv9sjxn36h7qk9vrs-bash-5.2p37/
/nix/store/00zrahbb32nzawrmv9sjxn36h7qk9vrs-bash-5.2p37
├── bin
│   ├── bash
│   └── sh -> bash
└── lib
    └── bash
        ├── accept
        ├── basename
        ├── csv
        ├── cut
        ├── dirname
        ...
        ├── sync
        ├── tee
        ├── truefalse
        ├── tty
        ├── uname
        ├── unlink
        └── whoami

Package (read-only file tree)

📦

Symbolic Links All Over The Place

$ ls -l /run/current-system/sw
lrwxrwxrwx 2 root root 55  1 Jan 1970  /run/current-system/sw -> /nix/store/1n2l2vz3sdzgxjilhqzndff9lmyvdg8c-system-path

$ ls -l /run/current-system/sw/
bin/    etc/    lib/    sbin/   share/

$ ls -l /run/current-system/sw/bin
...
lrwxrwxrwx  8 root root  70  1. Jan 1970  zstdcat -> /nix/store/n0crcrrdb0jlfmwidnv150vm33611xs3-zstd-1.5.7-bin/bin/zstdcat
lrwxrwxrwx  8 root root  71  1. Jan 1970  zstdgrep -> /nix/store/n0crcrrdb0jlfmwidnv150vm33611xs3-zstd-1.5.7-bin/bin/zstdgrep
lrwxrwxrwx  8 root root  71  1. Jan 1970  zstdless -> /nix/store/n0crcrrdb0jlfmwidnv150vm33611xs3-zstd-1.5.7-bin/bin/zstdless
lrwxrwxrwx  8 root root  69  1. Jan 1970  zstdmt -> /nix/store/n0crcrrdb0jlfmwidnv150vm33611xs3-zstd-1.5.7-bin/bin/zstdmt

System Packages

$ ls -l /etc/static/profiles/per-user/
lrwxrwxrwx 2 root root 60  1 Jan 1970  peter -> /nix/store/z8h5rbv29p6a7rxdjr31b3z6pwp0giv7-user-environment

$ tree /etc/static/profiles/per-user/peter/bin
/etc/static/profiles/per-user/peter/bin
├── accessdb -> /nix/store/qyiyrbrh1wnpa43w4vqn9r89w4q8b4bb-home-manager-path/bin/accessdb
├── apropos -> /nix/store/qyiyrbrh1wnpa43w4vqn9r89w4q8b4bb-home-manager-path/bin/apropos
├── catman -> /nix/store/qyiyrbrh1wnpa43w4vqn9r89w4q8b4bb-home-manager-path/bin/catman
├── codium -> /nix/store/qyiyrbrh1wnpa43w4vqn9r89w4q8b4bb-home-manager-path/bin/codium
├── ghostty -> /nix/store/qyiyrbrh1wnpa43w4vqn9r89w4q8b4bb-home-manager-path/bin/ghostty
├── git -> /nix/store/qyiyrbrh1wnpa43w4vqn9r89w4q8b4bb-home-manager-path/bin/git
...

User Packages

$ nix store gc -v
finding garbage collector roots...
deleting garbage...
deleting '/nix/store/v5cqs341xjgpqsx5c8pc2h8hz9mvwv3b-source'
deleting '/nix/store/zmnlm7rglapnp1mm8mz25jlbjgsckniy-nixos-system-bittner-25.11.20250719.c87b95e.drv'
deleting '/nix/store/x302k0rbgrry7vm94mkzvz1pzj9b2ng2-etc-nix-registry.json'
deleting '/nix/store/i1l6nqcknyzgpg77h3ysf7axspzhqzsl-nixos-system-bittner-25.11.20250719.c87b95e.drv'
deleting '/nix/store/al0hg0j2x9ikrniqxfmrfc0vjg3zxda5-etc.drv'
deleting '/nix/store/2q5svikixkmx7mfh3l1xrx4bm99a8ksr-etc.drv'
deleting '/nix/store/9ndq6wxadmlp85y22pcw92bbmdkf594y-etc-nix-registry.json.drv'
deleting '/nix/store/182c2qj20sfspzxcshd6p051qdx3mx5j-source'
...
deleting '/nix/store/cdirisw2j68pmr3anwvzy43q3zyfks44-plymouth-initrd-themes'
deleting '/nix/store/i69ii1nris373wqnzbl7hgmbns9f3f55-x86_64-unknown-linux-musl-gcc-14.3.0-lib'
deleting '/nix/store/i8zh98ynh9bn3gz244pispp81dzmw8n6-activation-script'
deleting '/nix/store/8q3vz0vk5bllxrqa0v5s1fxp9a0jmdpy-boot.json'
deleting unused links...
note: currently hard linking saves 99903.89 MiB
73449 store paths deleted, 34381.53 MiB freed

Garbage Collection

Cleanup of unreferenced packages (and derivations)

Language

Nix

42

number.nix

"foo"

string.nix

true

boolean.nix

~/.local/bin

path.nix

[ "foo" "bar" 42 false ]

list.nix

{ target = /etc/nixos; foo = 42; }

attribute-set.nix

x: x + 1

function.nix

{
  x = import ./number.nix;
}

imported-value.nix

Data Types, Operators, Built-ins

{
  # negation
  truth = !false;

  # string and paths
  a_str = "Dirs" + ./.config/user;
  a_path = /etc + "/nixos";

  # list concatenation
  l = [ 1 2 3 ] ++ [ "four" "five"];
  
  # logical and, logical or
  m = true && false; n = true || false;
  
  # update attribute set
  s = { x = 1; } // { y = 1; x = 2; };
  
  # has attribute (attribute set member)
  h = { x = 42; } ? x;
}

operators

{
  # available = builtins; 
  yes = true; no = false; nothing = null;
  system = builtins.currentSystem;
  epoch = builtins.currentTime;
  packages = builtins.storeDir;
  paths = builtins.nixPath;
  language = builtins.langVersion;
  version = builtins.nixVersion;
}

built-in constants

{
  d = builtins.readDir;
  f = builtins.readFile;
  e = builtins.getEnv "HOME";
  s = builtins.substring 0 3 "nixos";
  ...
}

built-in functions

{
  # numbers
  a = 1; f = 3.14;

  # strings
  text = "Hello Pi! ${f}";
  multi_line = ''
    Very long text ... with ${a}
  '';
  
  # paths
  x = /absolute/path/to
  y = ./relative/path
  z = ~/.config # path in home dir
}

datatype features

{ lib, ... }:

{
  imports = [
    ./file.nix
  ];

  options = {
    magicNumber = lib.mkOption {
      type = lib.types.number;
      default = 3.14;
    };
  };

  config = {
    boot.loader.grub.enable = true;
  };
}

module.nix

{ lib, ... }:

let
  preference = "GNOME";
in
{
  imports = [
    ./file.nix
  ];

  options = {
    desktop.style = lib.mkOption {
      type = lib.types.enum [ "Windows" "macOS" "GNOME" ];
      default = preference;
    };
  };
}

variables.nix

Nix Module

{
  console.keyMap = "it";

  services.xserver.xkb = {
    layout = "it";
    variant = "";
  };
}

implicit-config.nix

config

Nix Flake

$ nix flake show templates
$ nix flake init -t 'templates#utils-generic'
$ nix flake update
$ nix flake check
$ nix flake show
  • Keep flake content minimal
  • Let flake be only the entry point

Recommendation

{
  description = "Linux Day flake";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
  	nixos-hardware.url = "github:NixOS/nixos-hardware?ref=master";
  };

  outputs = { self, nixpkgs }: {
    nixosConfigurations = nixpkgs.lib.mapAttrs mkSystem systems;
  };
}

flake.nix

Use Cases

NixOS

I want a computer that works
$ nixos-rebuild --sudo --file ~/.config/nixos/default.nix
...
Done. The new configuration is /nix/store/j6a42...
  • Add more software (systemPackages, programs)
  • Split up config file when it gets too big
  • Start using Git, maybe ... 🧑‍💻 (... after breaking the setup)

🧑‍💻#1: Normal User

I want to run any software, instantly
$ nix run nixpkgs#neofetch
...
$ nix shell nixpkgs#cowsay nixpkgs#lolcat
$ cowsay 'Hello Linuxday!' | lolcat
 _________________
< Hello Linuxday! >
 -----------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
  • Install and run programs ad-hoc ("because I can!")
  • Split up config file when it gets too big
  • Switch to Flake-based NixOS setup (to fight flaky updates)
  • Maybe put configuration under version control (Git)

🧑‍💻#2: Power User

I manage a fleet of computers
$ nixos-rebuild switch --sudo --flake gitlab:painless-software/nixos-config
...
Done. The new configuration is /nix/store/j6a42...

$ nixos-rebuild test --sudo --flake .
building the system configuration...
Place your finger on the fingerprint reader
activating the configuration...

$ nix flake check
$ nix flake show
$ nix flake update

🧑‍💻#3: System Engineer

  • Split up config file when it gets too big
  • Use Flake-based setup (for more control about what is installed)
  • Manage nixos-config with Git, install using remote URL
  • Automate system updates (e.g. system.autoUpgrade)
I write software (incl. websites)
$ nix flake show templates
...
$ nix flake init -t templates#...
...

$ nix develop
...
$ go version
go version go1.25.1 linux/amd64
  • Python, Ruby, NodeJS, Rust, Golang, LaTeX, ...
  • Add a Flake file to every (serious) project
  • Use nix develop to start a development shell

🧑‍💻#4: Developer

{
  description = "A simple Go package";
  inputs.nixpkgs.url = "nixpkgs/nixos-25.11";
  outputs = { self, nixpkgs }:
  {
    packages = forAllSystems (system:
      let
        pkgs = nixpkgsFor.${system};
      in
        ...
    );
  };
  devShells = forAllSystems (system:
    default = pkgs.mkShell {
      buildInputs = with pkgs; [ go gopls gotools go-tools ];
    };
  );
}

flake.nix

I want a pythonic work environment
$ uv python install 3.14 --preview --default
$ which python
/home/peter/.local/bin/python
$ uv python upgrade --preview-features python-upgrade
All versions already on latest supported patch release

$ uv tool install pre-commit
$ uv tool install httpie
$ ls ~/.local/bin
http  httpie  https  pre-commit  python
$ uv tool list
...
$ uv tool upgrade --all

$ uvx pyclean
$ uvx copier update
  • Install only uv (but not any Python!) via systemPackages
  • Manage Python versions (and tools) locally using uv
  • Use nix-ld to fix the dynamic linking problem

👩‍💻#4a: Python Developer

{
  environment = {
    localBinInPath = true; # ~/.local/bin in PATH
    systemPackages = with pkgs; [
      uv # install Pythons in user-space using uv
    ];
  };

  programs.nix-ld = {
    enable = true;
    libraries = with pkgs; [
      libelf
      libjpg
      libpng
    ];
  };
}

configuration.nix

I create software for NixOS users
$ nix shell nixpkgs#nix-init
$ nix-init --url https://github.com/bittner/pyclean
...
$ nix build --file .

$ nix build nixpkgs#hello
$ ls -l
lrwxrwxrwx 1 peter users  56 25 Ott 15:42 result -> /nix/store/9mj...as2fis-hello-2.12.2
$ ./result/bin/hello
Hello, world!
  • Create Nix packages by writing them like a God
  • Maybe use tooling such as nix-init for "nixification"
  • Use nix build to build and locally verify a package setup

🧑‍💻#5: Package Maintainer

Structure + Strategy

NixOS

  • hosts/ – managed machines
  • roles/ – classes of machines (abstraction layer)
  • system/ – system-global configuration
  • home/ user configuration
  • user/ – common user configuration

Host-based Configuration

Manage user-specific software and settings ("dotfiles")

Home Manager

Declarative disk partitioning

disko-config.nix

$ sudo nix run github:nix-community/disko/latest -- \
   --mode destroy,format,mount \
   --flake gitlab:painless-software/nixos-config#example

Disko

💾

1.

Flake

Establish execution entrypoint (e.g. integrate hosts/).

Consider integrating QA tools, pre-commit and CI/CD early.

2.

Disko

Integrate Disko configuration. Verify installation process end-to-end.

Consider setting up functional tests with VMs.

4.

Refine

Delegate host setup to system/ configuration. Consider introducing roles/.

Consolitate home/  features in common user/ modules.

3.

Home Manager

Allow configuring settings and installing software for individual users.

Consider using LDAP  for a flexible, host-independent setup.

Iterative Development

🚀

Use Technology's QA Tools

$ copier copy gl:painless-software/cicd/config/nixos nixos-config
...

$ pre-commit install
...

$ git init

$ git add -v .
...

$ pre-commit

$ nix flake check
...

.gitlab-ci.yml

---
deadnix:
  extends: .nix
  script: nix run nixpkgs#deadnix -- --fail

statix:
  extends: .nix
  script: nix run nixpkgs#statix -- check

flake:
  extends: .nix
  script:
  - nix flake check
  - nix flake show

disko:
  extends: .nix
  script: nix run github:nix-community/disko/latest --
    --mode destroy,format,mount --dry-run --flake .#generic

tooling:
  extends: .megalinter
  variables:
    FLAVOR: documentation

Thank you!

for your precious time

Painless Software

Less pain, more fun.

Most static backgrounds from Unsplash (CC BY-SA)
Animated backgrounds from GIPHY (CC BY-SA)
Decorative icons are unicode (CC-0)

Wow!

This presentation

was made entirely

without any AI!