NixOS Quickstart

This is a guide I wrote for a friend to help with getting started in NixOS. The hope is to detail everything a reasonably experienced Linux user needs to know to get started.

It's not really finished, but I have put it here anyway. If you have any questions or suggestions for how to make this better, please contact us.

Installation

All of the commands in this section should be run as root with sudo.

Partitioning, Formatting, and Mounting

  1. Partition your disk(s) like you would any other system. The usual collection of partitioning tools are provided. (I recommend cfdisk.)

  2. Format your partitions using the various mkfs commands. LVM volumes and mdadm software RAID are also supported.

  3. Mount your partitions in /mnt the same way you want the system to mount them in /. Enable any swap devices you've created with swapon.

UEFI systems should use a GPT disk containing a >=512MiB EFI System partition. It should be formatted FAT32 (mkfs.fat -F 32) and mounted as /boot.

Installing

Before installing, you need a file /mnt/etc/nixos/configuration.nix which describes your system configuration. In later sections we will discuss this configuration, but for now you can generate a reasonable default like so:

nixos-generate-config --root /mnt

You can have a look at the resulting configuration if you'd like a sneak peek. (nano is provided for text editing.) For now, the defaults should be fine.

Once you're ready to install:

nixos-install

After the install finishes, you'll be asked for a password you can use to login as root after rebooting.

see also NixOS Manual: Part 1. Installation

The Nix Expression Language

Nix is a lazy, purely functional language which you'll be using to configure your system. Before we discuss configuration in detail, you should be familiar with its basic syntax.

Types

  • Strings:
    "Hello, world!"
    ''First Line
      Second Line
    ''
    
  • Integers and Floating-Point Numbers:
    1
    3.14
    
  • Filesystem Paths:
    /path/to/file
    ./hardware-configuration.nix
    ./. #! current directory: just `./` is not valid
    
    #! paths in angle brackets search the contents of $NIX_PATH. $NIX_PATH can
    #! have {key}={value} entries for special prefixes.
    
    #! `<nixpkgs>` is the path to your system's local copy of `nixpkgs`, located
    #! at `/nix/var/nix/profiles/per-user/root/channels/nixos`,
    <nixpkgs/pkgs/tools/misc/cowsay>
    
    #! and `<nixos-config>` is the path `/etc/nixos/configuration.nix`
    <nixos-config>
    
  • URLs (don't use these, they're deprecated!):
    https://github.com/NixOS/rfcs/blob/master/rfcs/0045-deprecate-url-syntax.md
    
  • Booleans:
    true
    false
    
  • Null:
    null;
    
  • Lists, whose values can be any type:
    [ 1 "two" [ 3.0 ] { four = 4; } ]
    
  • Sets (like dicts, maps), whose keys are strings and whose values can be any type:
    { a = 1; b = [ 2 ]; c = { x = 3; }; }
    
    #! keys can be quoted if they have special characters that can't be there
    #! normally
    { "blah..." = 1; }
    
    #! make sure you don't forget those semicolons!
    
  • Functions, which take exactly one argument and return the result of exactly one expression:
    #! these commands are performed in `nix repl` if you want to try them yourself.
    
    f = n: n + 1
    x = f 1 #: 2
    
    #! you can use currying to create functions which take multiple arguments
    f = first: second: first + second
    x = f 1 2 #: 3
    
    #! or you can destructure a set argument in the function declaration like so
    f = { first, second }: first + second
    x = f { first = 1; second = 2; } #: 3
    
    #! you can also accept a set with unspecified extra attributes (...), and
    #! specify defaults for optional attributes (attrib ? default)
    f = { first, second ? 2, ... }: first + second
    x = f { first = 1; irrelevant = 5; } #: 3
    
    #! and even access the whole set by name to get to those extra attributes
    #! (name@{ ... } or { ... }@name)
    f = input@{ first, second, ... }: first + second + input.third
    x = f { first = 1; second = 2; third = 3; } #: 6
    

Operators

set.x # attribute access
f x   # function application

-x # numerical negation

# attribute existence testing (true if `set` contains an attribute `x`)
set ? x

list ++ list # list concatenation

x * y / z # multiplication and division
x + y - z # addition and subtraction

!a # logical negation

# set merging (a set with both sets' attributes, if both sets have an attribute
# with the same key then `set2` takes precedence)
set1 // set2

a<b<=c>=d>e  # comparison
a == b != c  # equality testing
a && b       # logical conjunction
a || b       # logical disjunction

# logical implication (if `a` is true, `b` needs to be true or else the result
# is false)
a -> b

Builtins

abort s # no return value. stop evaluation and print error string

baseNameOf s # part of `s` after its last '/' character

derivation inputs # a derivation. will be discussed later

dirOf s # part of `s` preceding `baseNameOf s`

fetchTarball url # path of unpacked .tar.(gz|xz|bz2) downloaded from `url`

import path # the expression at `path`

# whether `x` is null. this is deprecated because you can just write `x == null`
isNull x

map f list # list of each `f x` for each `x` in `list`

removeAttrs set list # `set` with each attribute in `list` removed

throw s # no return value. `abort s` but softer

toString x # `x` converted to a string

builtins # a set containing more builtins

The remaining built-in functions are kept in builtins to prevent clutter. The contents of that set are described in Nix Manual: 15.5. Built-in Functions.

Other Features

# comments, as you've probably noticed, begin like this
/* multi-line comments, however,
   use these */

# bring a set's attributes into scope
s = { a = 1; };
x = with s; [ a ]; #: [ 1 ]

# define some temporary local variables to use in an expression
x = let
  a = 1;
  b = 2;
in a + b; #: 3

# allow a set to reference its own attributes
x = rec {
  a = 1;
  b = a + 1;
}; #: { a = 1; b = 2; }

# inherit some variables from the surrounding scope
x = rec {
  a = 1;
  b = {
    inherit a;
  }
}; #: { a = 1; b = { a = 1; }; }

# or from another set
x = rec {
  a = { b = 1; };
  inherit (a) b;
}; #: { a = { b = 1; }; b = 1; }

see also Nix Manual: Part IV. Writing Nix Expressions or NixOS Wiki: Nix Expression Language

Modules

A .nix file contains a single expression, which means it can be evaluated to a single value. A module is a particular kind of Nix expression which can

  • import other modules,
  • declare a set of options you can change to configure its behaviour,
  • and define a set of options that were declared in other modules.

It's a set, or a function that returns a set, that can look like this:

{ ... }: {
  imports = [
    # import paths
  ];

  options = {
    # option declarations
  };

  config = {
    # option definitions
  };
}

If it doesn't need to declare any options, it can also look like this:

{ ... }: {
  imports = [
    # import paths
  ];

  # option definitions
}

Or, if it doesn't need any imports and takes no arguments, it can even look like this:

{
  # option definitions
}

Import paths can lead to modules themselves, or to directories (in which case the module named default.nix inside that directory is imported).

If the module is a function, it's passed a set with the attributes

Modules are the building block for configuration in NixOS. Each system's configured state is defined by a set of option definitions.

see also NixOS Manual: 50. Writing NixOS Modules

configuration.nix by Example

Your configuration.nix is a module. It defines some of the options declared in modules elsewhere, usually for the most part ones in <nixpkgs/nixos/modules>. You can search for information about these options on this page.

When you alter your configuration, you can type nixos-rebuild switch to switch to it and make it the new default. You can also check the NixOS Commands page for different ways to use nixos-rebuild.

What follows are some example configurations to hopefully give you some helpful context for how NixOS modules are configured.

see also NixOS Manual: Chapter 5. Configuration Syntax

A Basic Desktop

# configuration.nix

{ pkgs, ... }: {
  imports = [
    ./hardware-configuration.nix
  ];

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

  networking = {
    # it's recommended to disable DHCP globally and enable it seperately for
    # each interface
    useDHCP = false;

    interfaces.enp1s0.useDHCP = true;
  };

  services = {
    openssh.enable = true;

    xserver = {
      enable = true;

      desktopManager.gnome3.enable = true;

      # NixOS's default display manager is LightDM. however, GDM fits better
      # with GNOME
      displayManager.gdm.enable = true;
    };
  };

  users = {
    # disallows users and groups from being modified outside of the system 
    # configuration, which is useful if you only ever want to configure users
    # inside your `configuration.nix`
    mutableUsers = false;

    users.raccoon = {
      # regular interactive user with a home directory etc.
      isNormalUser = true;

      # can use `sudo`
      extraGroups = [ "wheel" ];

      # you can generate a hashed password for this option with
      # `mkpasswd -m sha-512`
      hashedPassword = "/*snip*/";

      # note the `with pkgs;` so you don't have to type `pkgs.vim` etc. for
      # each package
      packages = with pkgs; [ vim ];
    };
  };

  system.stateVersion = "20.09";
}

This configuration is based on the default generated one, with a few simple additions. It will give you

  • the systemd-boot bootloader,
  • a user named raccoon with vim in their environment and the permission to use sudo,
  • DHCP for enp1s0,
  • an ssh service,
  • and a GNOME 3 desktop.

Note that I've omitted the contents of hardware-configuration.nix. This file includes some important hardware-dependent configuration such as necessary filesystem mounts and kernel modules. Apart from being created automatically, it is not treated specially; you could specify all of those things yourself, too.

A Static Webserver

This is a configuration for a webserver that serves static files from /srv/www/<subdomain>. The first part includes the configuration.nix itself and some hardware configuration for a system running on a Linode. The second part configures the actual webserver, and uses a little custom function in the process.

Part 1

# configuration.nix

{
  imports = [
    ./boot.nix
    ./fs.nix
    ./net.nix
    ./users.nix
    ./web
  ]

  services.openssh.enable = true;

  system.stateVersion = "20.09";
}
# boot.nix

{ modulesPath, ... }: {
  imports = [
    # some defaults for virtual machines
    (modulesPath + "/profiles/qemu-guest.nix")
  ];

  boot = {
    # serial console for Linode's web shell
    kernelParams = [ "console=ttyS0" ];
    
    # for SATA and SCSI
    initrd.availableKernelModules = [ "ahci" "sd_mod" ];

    loader.grub = {
      enable = true;

      # configure but don't install GRUB (Linode already manages it)
      device = "nodev";

      # put kernel in /boot
      copyKernels = true;

      # use partition labels, drive UUIDs can change
      fsIdentifier = "label";

      extraConfig = "serial; terminal_input serial; terminal_output serial";
    };
  };
}
# fs.nix

{
  fileSystems = {
    "/" = {
      device = "/dev/disk/by-label/nixos";
      fsType = "ext4";
    };

    "/srv" = {
      device = "/dev/disk/by-label/srv";
      fsType = "ext4";
    };
  };

  swapDevices = [
    { device = "/dev/disk/by-label/swap"; }
  ];
}
# net.nix

{
  networking = {
    useDHCP = false;

    # the physical location of the same network interface can change between
    # boots, use `ethX` instead of `enpXsY` for interface names
    usePredictableInterfaceNames = false;
    interfaces.eth0.useDHCP = true;
  };
}
# users.nix

{
  users = {
    mutableUsers = false;

    users.admin = {
      isNormalUser = true;
      extraGroups = [ "wheel" ];
      hashedPassword = "/*snip*/";

      # ssh auto-login with public key
      openssh.authorizedKeys.keys = [
        "/*snip*/"
      ];
    };
  };
}

Part 2

In this part we'll set up nginx to use TLS and host two subdomains: www and files. We'll write a function to help us configure both subdomains the same way without repeating ourselves.

# web/default.nix

{
  imports = [
    ./www.nix
    ./files.nix
  ];

  networking.firewall.allowedTCPPorts = [ 80 443 ];

  # use ACME to get certificates for TLS (with Let's Encrypt as the default CA)
  security.acme = {
    acceptTerms = true;
    email = "me@example.com";
  };

  services.nginx = {
    enable = true;

    # by default, nginx will restart instead of reload on config change
    enableReload = true;

    # some sane defaults
    recommendedOptimisation = true;
    recommendedTlsSettings = true;
    recommendedGzipSettings = true;
    recommendedProxySettings = true;
  };
}
# web/vhost.nix

# this is a function that returns the settings we want each subdomain to have by
# default, to help us not repeat ourselves
sub: let
  domain = "example.com";
  root = /srv/www;
# notice how we can nest `let ... in`. it's helpful for temp values.
# we need `domain` and `root` to define `subdomain` and `subroot` but we don't
# need them elsewhere
in let
  subdomain = if sub == null
    then domain
    else "${sub}.${domain}";
  subroot = if sub == null
    then root
    else "${root}/${sub}";
# using `rec` because `definition` references `name` and `settings`
in rec {
  name = subdomain;
  settings = {
    forceSSL = true;
    enableACME = true;
    root = "${subroot}";

    locations."/".tryFiles = "$uri $uri/ =404";
  };

  # set that looks like `services.nginx.virtualHosts` for convenience
  definition = { ${name} = settings; };
}
# web/www.nix

let
  vhost = import ./vhost.nix;
in let
  root = vhost null;
  www = vhost "www";
in {
  services.nginx.virtualHosts = www.definition // {
    # make `example.com` redirect to `www.example.com`
    "${root.name}" = root.settings // {
      locations."/".return = "301 https://${www.name}$request_uri";
    };
  };
}
# web/files.nix

{ services.nginx.virtualHosts = (import ./vhost.nix "files").definition; }

Modules with Declarations

In this section, we'll look at an example of a module from <nixpkgs/nixos/modules> that declares some options to provide a session for the i3 window manager.

(Comments and formatting are my own)

# <nixpkgs/nixos/modules/services/x11/window-managers/i3.nix>

{ config, lib, pkgs, ... }: with lib; let
  # it's convention to define `cfg` as your own config
  cfg = config.services.xserver.windowManger.i3;
in {
  options = {
    services.xserver.windowManager.i3 = {
      # lib.mkEnableOption generates an option that accepts a bool and has the
      # description "Whether to enable {arg}"
      enable = mkEnableOption "i3 window manager";

      # lib.mkOption is more general and accepts a whole set of options.
      # in this case, a default value, a description, and a lib.types type
      configFile = mkOption {
        default = null;
        type = with types; nullOr path;
        description = ''
          Path to the i3 configuration file.
          If left at the default value, $HOME/.i3/config will be used.
        '';
      };

      extraSessionCommands = mkOption {
        default = "";
        type = types.lines;
        description = ''
          Shell commands executed just before i3 is started.
        '';
      }

      # sometimes packages have multiple variants you can use in place of the 
      # original.
      # in these cases, an option `package` is usually supplied to configure
      # what the module puts in your environment.
      package = mkOption {
        type = types.package;
        default = pkgs.i3;
        defaultText = "pkgs.i3";
        example = "pkgs.i3-gaps";
        description = ''
          i3 package to use.
        '';
      };

      extraPackages = mkOption {
        type = with types; listOf package;
        default = with pkgs; [ dmenu i3status i3lock ];
        example = literalExample ''
          with pkgs; [
            dmenu
            i3status
            i3lock
          ]
        '';
        description = ''
          Extra packages to be installed system wide.
        '';
      };
    };
  };

  # lib.mkIf only applies configuration if the predicate is true
  config = mkIf cfg.enable {
    environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages;

    environment.etc."i3/config" = mkIf (cfg.configFile != null) {
      source = cfg.configFile;
    };

    services.xserver.windowManager.session = [
      {
        name = "i3";
        start = ''
          ${cfg.extraSessionCommands}

          # lib.optionalString returns "" if the predicate is false
          ${cfg.package}/bin/i3 ${optionalString (cfg.configFile != null)
            "-c /etc/i3/config"
          } &
          waitPID=$!
        '';
      };
    ];
  };

  imports = [
    # lib.mkRemovedOptionModule returns a path to a module with a single option
    # that throws an error message
    (mkRemovedOptionModule [ "services" "xserver" "windowManager" "i3-gaps" "enable" ]
      "Use services.xserver.windowManager.i3.enable and set services.xserver.windowManager.i3.package to pkgs.i3-gaps to use i3-gaps.")
  ];
}

As you can see, it's pretty simple. If you enable it, you get the i3 package and an X session that runs the provided binary. Like most modules, it makes heavy use of the library provided in <nixpkgs/lib>.

Packages and Derivations

Like modules, packages are just expressions with a few specific qualities.

A package is a function that takes in a set of attributes (normally from <nixpkgs>) and outputs a derivation.

A derivation represents a build action. Derivations are a good part of what makes Nix packages deterministic: they are defined by all the inputs used to make them, so the same package given different inputs will necessarily be identified as an entirely different derivation.

Let's take a look at a simple package, hello.

# <nixpkgs/pkgs/applications/misc/hello/default.nix>

{ stdenv, fetchurl }: stdenv.mkDerivation rec {
  pname = "hello";
  version = "2.10";

  src = fetchurl {
    # `mirror://` is just a shorthand for one of the mirrors specified in
    # <nixpkgs/pkgs/build-support/fetchurl/mirrors.nix>
    url = "mirror://gnu/hello/${pname}-${version}.tar.gz";
    # a hash is a *required* argument to fetchurl, in order to guarantee that
    # the URL will always point to the same file
    sha256 = "0ssi1wpaf7plaswqqjwigppsg5fyh99vdlb9kzl7c9lng89ndq1i";
  };

  # run the check phase, just `make check` by default
  doCheck = true;
  
  meta = with stdenv.lib; {
    description = "A program that produces a familiar, friendly greeting";
    longDescription = ''
      GNU Hello is a program that prints "Hello, world!" when you run it.
      It is fully customizable.
    '';
    homepage = "https://www.gnu.org/software/hello/manual/";
    changelog = "https://git.savannah.gnu.org/cgit/hello.git/plain/NEWS?h=v${version}";
    license = licenses.gpl3Plus;
    maintainers = [ maintainers.eelco ];
    platforms = platforms.all;    
  }
}

As you can see, the derivation here is created using stdenv.mkDerivation. Functions like these are wrappers around the builtin function derivation, which I will not be documenting here as it would be unproductive.

The attributes you can provide to mkDerivation are numerous; they control dependencies, build phases, and other aspects of the build. They're described in full at Nixpkgs Manual: 6.3. Specifying dependencies.

The [Nixpkgs Manual] provides lots of other helpful documentation for writing Nix packages, including information on

Commands

Now that we've taken a tour of some essential Nix concepts, it's time to discuss how to use its various commandline utilities.

Only short summaries of the most useful commands will be provided for reference purposes. Check the manpages for full descriptions.

Nix Commands

nix-channel

Interacts with channels (Nix's equivalent to a repository: URL to a set of Nix expressions).

  • --list lists added channels,
  • --add <url> [name] adds a channel,
  • --update [name...] downloads an updated copy of added channels,
  • --remove <name> removes a channel,
  • --rollback reverts the most recent --update.

nix-collect-garbage

Deletes all Nix store paths not currently being used.

Additionally, can delete old generations of user profiles:

  • {--delete-old | -d} deletes all non-current generations,
  • --delete-older-than <period> deletes all generations older than the period.

nix-env

Interacts with profiles, normally user environments (sets of packages available to a given user).

To specify a user other than yourself, you can use {--profile | -p} /nix/var/nix/profiles/per-user/<username>. To interact with the system profile (in order to use the generations-related actions), you can even specify /nix/var/nix/profiles/system.

For commands accepting packages as arguments, package names are provided as regular expressions, optionally followed by a dash and a version, e.g. hello-2.10. If the option -A is provided, attribute names as in pkgs.<name> are used instead.

  • {--query | -q} <name...> queries installed packages (pass -a to query available but not installed packages),
  • {--install | -i} <name...> installs packages,
  • {--uninstall | -e} <name...> uninstalls packages,
  • --set <name> uninstalls everything and installs just the specified package,
  • {--upgrade | -u} [name...] upgrades packages (all installed packages if none specified),
  • --list-generations lists generations,
  • {--switch-generation | -G} <generation> switches to the specified generation,
  • --delete-generations <generation...> deletes generations (specify a generation, an age like "30d" to select all older generations, a count like "+5" to keep all but the most recent 5 generations, or "old" to delete all but the currently active generation),
  • --rollback rolls back to the previous generation.

nix-prefetch-url

Prints the hash of the file downloaded from the specified URL. Useful when writing a package with a fetchurl input.

nix-shell

Starts an interactive shell after building the dependencies of a derivation. If the path to a derivation is not specified, shell.nix or default.nix (in order of preference) in the current directory is used instead.

# shell.nix

{ pkgs ? import <nixpkgs> {} }: pkgs.mkShell {
  inputsFrom = with pkgs; [ hello ];
  buildInputs = with pkgs; [ python3 ];
  shellHook = ''
    python
  '';
}

This is an example shell.nix that uses the special derivation function mkShell. If you run nix-shell in the same directory, you will enter a shell with python3 and all the inputs needed to build hello, and the shell will immediately start run the python interpreter.

Instead of specifying a derivation, you can also use nix-shell -p to specify a list of packages you want in the environment. If you just wanted the python interpreter, you could use nix-shell -p python3, or even nix-shell -p python3 --run python to enter the interpreter automatically.

see also Nix Manual: Part III. Package Management

NixOS Commands

nixos-enter, nixos-generate-config, and nixos-install all accept a --root <path> argument to change the path to the NixOS installation. By default, nixos-generate-config acts on /, and the others act on /mnt.

nixos-enter

Chroots into a NixOS installation.

Without a command, enters an interactive shell.

  • {--command | -c} <cmd> executes a command in the shell,
  • -- <args...> executes a command not in the shell.

nixos-generate-config

Generates configuration.nix if it doesn't already exist, and generates or updates hardware-configuration.nix.

nixos-install

Installs NixOS from an existing configuration.nix.

  • --no-root-passwd do not ask for a root password. Disables root login unless otherwise specified in configuration.nix.

nixos-option

Lists the properties of the specified option.

nixos-rebuild

Builds a system from the current configuration.

The following need root:

  • boot builds the new configuration and makes it the default boot target,
  • test builds the new configuration and modifies the currently running system to match it,
  • switch builds the new configuration, makes it the default boot target, and modifies the currently running system to match it.

While the following can be run as non-root:

  • build builds the new configuration and makes a symlink result,
  • dry-build shows what would be performed by build or boot but otherwise does nothing,
  • dry-activate shows what would be performed by test or switch but otherwise does nothing,
  • build-vm builds the new configuration and makes a symlink result, with a script result/bin/run-<hostname>-vm which runs a qemu virtual machine for the resulting system,
  • build-vm-with-bootloader does the same as build-vm but for testing purposes uses the normal configured bootloader inside the virtual machine.

see also NixOS Manual: Chapter 3. Changing the Configuration

nixos-version

Prints the version.