Skip to main content

Getting Started with NixOS

12 mins


Introduction #

NixOS is a Linux distribution based on the Nix package manager. The main reason for using NixOS is the declarative reproducibility of your system.

NixOS allows you to declare pretty much everything about your system inside a single document. This document allows for the reproducable setup that can be used on any system.

Whenever this document gets updates, NixOS will generate a new generation which can be booted into. Since NixOS is known for being unbreakable, it will also store the previous generations just in case the new changes are causing issues.

Unlike the average Linux destribution, NixOS stores all it’s packages inside the /nix/store. Directory /lib, /usr/lib, /bin and /usr/bin are pretty much non-existant.

In this guide I would go into to much detail about setting up the installatia media but by following the steps below, you’ll end up with a decent understanding on how to configure your own declarative reproducible NixOS setup. I highly recommend that you use these notes in conjunction with the mini-course video to learn more about NixOS.

Getting Started #

Either follow the Calamares Installer or the step below:

Partitioning

  • Open the terminal and switch to the root user su
  • Run the commands below:
    • This will format the disk and create the partitions
      • Change 8GiB swap to your personal preference. Or remove it and end the root partition at 100%.
    • Choose between the two options between “< >”. Left option is legacy boot, right option is UEFI.
    • Change /dev/sda to the disk used to install NixOS.
  # parted /dev/sda -- mklabel <msdos/gpt>
  # parted /dev/sda -- mkpart primary <1MiB -8GiB/512MiB -8GiB> 
  # parted /dev/sda -- mkpart primary linux-swap -8GiB 100%

  /* extra for UEFI */
  # parted /dev/sda -- mkpart ESP fat32 1Mib 512MiB
  # parted /dev/sda -- set 3 esp on

  # mkfs.ext4 -L nixos /dev/sda1
  # mkswap -L swap /dev/sda2

  /* extra for UEFI */
  # mkfs.fat -F 32 -n boot /dev/sda3

Mounting

  # mount /dev/disk/by-label/nixos /mnt

  /* extra for UEFI */
  # mkdir -p /mnt/boot
  # mount /dev/disk/by-label/boot /mnt/boot

  # swapon /dev/sda2

Initial Configuration #

  • Generate a default configuration:
    • # nixos-generate-config --root /mnt
  • Location of configuration.nix:
    • # cd /mnt/etc/nixos

Configuration.nix #

General

  • Argument on how to evaluate config:
    • {config, pkgs, ...}:
  • Pull in other files used within the config:
    • import = [./hardware-configuration.nix];

Boot

Legacy

  • Only viable if dualbooting linux distributions*
  # Default Grub setup
  boot.loader.grub.enable = true;
  boot.loader.grub.version = 2;
  boot.loader.grub.device = "/dev/vda";
  # Dual booting made easy (Optional)
  boot.loader.grub.useOSProber = true;
  # Dual booting made a bit harder (Extra Optional)
  boot.loader.grub.extraEntries = ''
    menuentry "Windows 10" {
      chainloader (hd0,1)+1
    }
  '';

UEFI

  • Used for larger boot drives and dual booting with Windows*
  # Default UEFI setup
  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;
  
  # Dual Booting using grub
  boot.loader = {
    efi = {
      canTouchEfiVariables = true;
      efiSysMountPoint = "/boot/efi"; # /boot will probably work too
    };
    grub = {                          # Using grub means first 2 lines can be removed
      enable = true;
      #device = ["nodev"];            # Generate boot menu but not actually installed
      devices = ["nodev"];            # Install grub
      efiSupport = true;
      useOSProber = true;             # Or use extraEntries like seen with Legacy
    };                                # OSProber will probably not find windows partition on first install
  };

Extras

  • Some extra useful boot parameters
  { pkgs, ... }:

  {
    boot ={
      kernelPackages = pkgs.linuxPackages_latest;       # Get latest kernel
      initrd.kernelModules = ["amdgpu"];                # More on this later on (setting it for xserver)
      loader = {
        grub = {
          configurationLimit = 5;                       # Limit stored system configurations.
        };                                              # Also exists for systemd-boot
        timeout = 5;                                    # Work for grub and efi boot, time before auto-boot
      };
    };
  }

Internationalisation

  • Locales, Layouts and Options
  # Clock
  time.timeZone = "Belgium/Brussels";
  # Locale
  i18n.defaultLocale = "en_US.UTF-8";
  i18n.extraLocaleSettings = {
    LC_TIME = "nl_BE.UTF-8";
    LC_MONETARY = "nl_BE.UTF-8";
  };
  # TTY layout
  console = {
    font = "...";
    keyMap = "...";                           # us / fr / azerty / etc...
  };
  # XServer layout (possibly also sets console now)
  services.xserver.layout = "..."             # us / fr / be / etc..
  # Extra keyboard settings:
  services.xserver.xkbOptions = "eurosign:e"; # For example adds €

DE/WM

  • Default
  services.xserver.enable = true;
  services.xserver.displayManager.sddm.enable = true;
  services.xserver.desktopManager.plasma5.enable = true;
  • Customized
  services = {
    xserver = {
      enable = true;
      displayManager = {
        lightdm.enable = true;
        defaultSession = none+bspwm;
      };
      desktopManager.xfce.enable = true;
      windowManager.bspwm.enable = true;
    };
  };

Hardware

  • Example for sound and bluetooth
    sound = {
      enable = true;
      mediaKeys.enable = true;
    };
    services = {
      pipewire = {
        enable = true;
        alsa = {
          enable = true;
          support32Bit = true;
        };
        pulse.enable = true;
      };
    };
    hardware = {
      bluetooth = {
        enable = true;
        hsphfpd.enable = true;         # HSP & HFP daemon
        settings = {
          General = {
            Enable = "Source,Sink,Media,Socket";
          };
        };
      };
    };
  • Example of libinput for touchpads
  services.xserver.libinput = {
    enable = true;
    #tapping = true;
    #naturalScrolling = true;
    #...

Users

  • Example on how to create a user and add them to groups.
  users.users.<name> = {
    isNormalUser = true;
    extraGroups = [ "wheel" "video" "audio" "networkmanager" "lp" "scanner"]
    #initialPassword = "password";
    #shell = pkgs.zsh;
  };

Packages

  • Example of how to declare packages
  environment.systemPackages = with pkgs; [
    vim
    wget
    git
    #pkgs.firefox
    firefox

StateVersion

  • No need to touch this.
  • Nothing to do with the version of the system.
  • Just tells the version of state/config
  • Can be updated to a stable version if you are really sure.

Hardware-configuration.nix #

Generate

  • Also get automatically generated with:
    • # nixos-generate-config --root /mnt
  • Should detect mounted drives, device parts, kernelModules, etc.. that are needed
  • Can be deleted and regenerated with:
    • # nixos-generate-config

File System

  • $ sudo blkid
  • or just look in gparted
  fileSystems."/" =
    { device = "/dev/disk/by-uuid/e97ad9a8-d84f-4710-b8c9-cfa7707510ca";
      fsType = "ext4";
    };

  #fileSystem."/" =
  #  { device = "/dev/disk/by-label/nixos";
  #    fsType = "ext4";
  #  };

Networking

  • Network card details
    • Deprecated but keep: networking.useDHCP = false;
    • Just internet via ethernet: networking.interfaces.<networkcard-id>.useDHCP = true;
  networking = {
    #hostName = "nixos";
    #networkmanager.enable = true;
    interfaces ={
      enp0s3 = {
        #useDHCP = true;
        ipv4.addresses = [ {         # Ofcourse not compatible with networkmanager
          address = "192.168.0.50";
          prefixLength = 24;
        } ];
      };
    };
    defaultGateway = "192.168.0.1";
    nameservers = [ "1.1.1.1" ];
  };

Installation #

  • For the initial installation:
    • # nixos-install
  • After applying changes to your configuration.nix:
    • # nixos-rebuild switch
  • At the end of the installation process, pick a root password.
  • Reboot
  • If users.users.<user>.initialPassword was not set, you will need to do this now via the TTY
    • Ctr + Alt + F<1-12> -> Log in with root
    • # passwd <user>
    • Ctrl + Alt + F1 or F7 -> Log into graphical environment with user

Package Management #

Nix packages and options can be found here: packages | options

  • Install individually with Nix Package Manager
    • $ nix-env -iA nixos.<package>
    • $ nix-env --uninstall <package>
  • Temporarily install packages using $ nix-shell -p <package>

Declaring Packages

  • Install systemwide packages with configuration.nix
  environment = {
    systemPackages = with pkgs; [
      plex
      superTux
    ];
  };

  nixpkgs.config.allowUnfree = true;

Declaring Options

  • Some packages will also have options to configure it further
  services = {
    plex = {
      enable = true;
      openFirewall = true;
    };
  };

Declaring Options

  • Values that can change often or you want to use multiple times
  let
    rofi-theme = {
      "*" = {
        bg = "#FFFFFF";
      };
    };
  in
  {
    programs.rofi = {
      enable = true;
      theme = rofi-theme;
    };
  }

Declaring Options

  • Change packages that already exist in nix
  nixpkgs.overlays = [
    (self: super: {
      sl = super.sl.overrideAttrs (old: {
        src = super.fetchFromGitHub {
          owner = "mtoyoda";
          repo = "sl";
          rev = "923e7d7ebc5c1f009755bdeb789ac25658ccce03";
          sha256 = "0000000000000000000000000000000000000000000000000000";
        };
      });
    })

    (self: super: {
      discord = super.discord.overrideAttrs (
        _: { src = builtins.fetchTarball {
          url = "https://discord.com/api/download?platform=linux&format=tar.gz";
          sha256 = "0000000000000000000000000000000000000000000000000000"; #52 0's
        }; }
      );
    })
  ];

Applying

  • $ sudo nixos-rebuild switch

Extras #

Update & Upgrade

  1. $ nix-channel --add https://nixos.org/channels/nixos-21.11 OR $ nix-channel --update
  2. Next rebuild,use the –upgrade flag $ sudo nixos-rebuild --upgrade

If Apps are installed through nix-env $ nix-env -u '*'

Garbage Collections

  • Remove undeclared packages, dependencies and symlinks: $ nix-collect-garbage
  • Remove above of older generations: $ nix-collect-garbage --delete-old
    • List generations: $ nix-env --list-generations
  • Remove specific generations or older than … days:
    • $ nix-env --delete-generations 14d
    • $ nix-env --delete-generations 10 11
    • Optimize store: $ nix-store --gc
  • All in one:$ nix-collect-garbage -d

Inside configurations.nix

  nix = {
    settings.auto-optimise-store = true;
    gc = {
      automatic = true;
      dates = "weekly";
      options = "--delete-older-than 7d"
    };
  };

Home-Manager #

Home-Manager is configuration.nix but for the user environment. Instead of installing packages system-wide, it will install packages only for a specified user. It has plenty more options to declare if your configuration.nix and it’s a great way to configure and manage your ~/.config dotfiles. All configurations options can be found in the Home-Manager Appendixes .

Home-Manager can be set up in multiple way. Below you will find some of the option on how to implement it in your configuration.

Before getting started with Home-Manager, a nix-channel will need to be created so the system can make use of the new configuration options. If you plan on using Home-Manager as a NixOS-module, run the commands below as root.

Unstable channel: $ nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager Stable channel: nix-channel --add https://github.com/nix-community/home-manager/archive/release-<version.number>.tar.gz home-manager And update the channels with: $ nix-channel --update

Standalone #

Installation is done by running the command: $ nix-shell '<home-manager>' -A install You can nog edit the home.nix file in ~/.config/nixpkgs/home.nix

Module #

Implement the options below into your configuration.nix. You can afterwards nixos-rebuild switch.

  let
  in
  {
    imports = [ <home-manager/nixos> ];

    users.users.<name> = {
      isNormalUser = true;
    }

    home-manager.users.<name> = { pkgs, }: {
      # declared packages. for example:
      home.packages = [ pkgs.atool pkgs.httpie ];
    };
  }

Configuration #

Declaring packages

  home.packages = with pkgs; [
    firefox
  ];

  services.dunst = {
    enable = true;
  };

To apply the changes to the standalone version: $ home-manager switch

Dotfiles

  • Implement existing config inside the nix file
  home.file = {
    ".config/alacritty/alacritty.yml".text = ''
      {"font":{"bold":{"style":"Bold"}}}
    '';
  };
  • Implement with files saved locally
  home.file.".doom.d" = {
    source = ./doom.d;
    recursive = true;
    onChange = builtins.readFile ./doom.sh;
  };
  home.file.".config/polybar/script/mic.sh"={
    source = ./mic.sh;
    executable = true;
  };
  • Declarativly with existing options,\
  {
    xsession = {
      windowManager = {
        bspwm = {
          enable = true;
          rules = {
            "Emacs" = {
              desktop = "3";
              follow = true;
              state = "tiled";
            };
            ".blueman-manager-wrapped" ={
              state = "floating";
              sticky = true;
            };
          };
        };
      };
    };
  }

Flakes #

Flakes are a fully supported feature of the NixOS. With flakes you can specify code dependencies declaratively. These will be stored inside a flake.lock file. An example of a code dependency is home-manager. Flakes make rebuilding and updating the whole system a lot easier. Arguably it makes it way easier to build your own config. You can save multiple configs inside one file and share them easily with others using something like GitHub.

It’s highly recommended that you have a look at the wiki article about flakes.

Setup #

Inside the configuration.nix add:

  nix = {
    package = pkgs.nixFlakes;
    extraOptions = "experimental-features = nix-command flakes";
  };

Now choose a location where you want to store the flake file and run:

  • $ nix flake init Inside the flake.nix file you will find the options inputs and outputs. The inputs are used to declare your dependencies. Of of these dependencies is, for example, the nix packages itself.
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
  };

The outpus are the arguments that can be used that reference the imputs. This can be:

  • Everything you imported
  • Packages / configurations / modules / options / etc …

Configuration #

An example of a basic flake.nix file is:

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    #nixpkgs-unstable.url = "github:nixos/nixpkgs/nixpkgs-unstable";
  };
  outputs = { nixpkgs, home-manager,  }:
    let
      system = x86_64-linux;
      pkgs = import nixpkgs {
        inherit system;
        config.allowUnfree = true;
      };

      lib = nixpkgs.lib;
   in {
     nixosConfigurations = {
       <user> = lib.nixosSystem {
         inherit system;
         modules = [ ./configuration.nix ];
       };
       #<second user> = lib.nixosSystem {
         #inherit system;
         #modules = [ ./configuration.nix ];
       #};
     };
  }

In this example the configuration.nix and hardware-configuration.nix file is move to the same directory as the flake.nix file. To update the system using the flake.nix file run: $ sudo nixos-rebuild switch --flake .#

Home-Manager #

Once again, Home-Manager can be set up in multiple ways inside the flake.

Seperate

  {
    inputs = {
      #other inputs
      home-manager = {
        url = github:nix-community/home-manager;
        inputs.nixpkgs.follows = "nixpkgs";
      };
    };
    outputs = { self, nixpkgs, home-manager, ... }:
      let
        #variables
      in {
        #other outputs
        hmConfig = {
          <user> = home-manager.lib.homeManagerConfiguration {
            inherit system pkgs;
            username = <user>;
            homeDirectory = /home/<user>;
            #stateVersion = "22.05";  # If there is any complaining about differing stateVersions, specifically state here.
            configuration = {
              imports = [
                /home/<user>/.config/home/home.nix
              ];
            };
          };
        };
      };
  }

Inside nixosConfigurations

  {
    inputs = {
      #other inputs
      home-manager = {
        url = github:nix-community/home-manager;
        inputs.nixpkgs.follows = "nixpkgs";
      };
    };
    outputs = { self, nixpkgs, home-manager, ... }:
      let
        #variables
      in {
        nixosConfigurations = {
          <user> = lib.nixosSystem {
            inherit system;
            modules = [
              ./configuration.nix
              home-manager.nixosModules.home-manager {
                home-manager.useGlobalPkgs = true;
                home-manager.useUserPackages = true;
                home-manager.users.<user> = {
                  imports = [ ./home.nix ];
                };
              }
            ];
          };
        };
      };
  }

Build

If you chose for the seperate options, you will have to rebuild your system once for home-manager and once for flake itself. You will first need to build your home-manager config and afterwards switch.

  • $ nix build .#hmConfig.<user>.activationPackage
  • $ ./result/activate These command only need to be run on your initial installation. From now on you can switch to the new home-manager configuration using: home-manager switch --flake .#<host>

If you chose for the nixosConfigurations module just rebuild as usual: $ sudo nixos-rebuild switch --flake .#<host>

Update Since all dependencies are now declared, you will first need to update the flake.lock file before you can rebuild the system.

  • $ nix flake update
  • $ sudo nixos-rebuild switch --flake .#<host>

Conclusion #

By now, you should have a decent grasp of how NixOS works. If some notes are not clear, I highly recommend that you watch the 3 hour mini-course where I go into much more detail.

Matthias Benaets
Author
Matthias Benaets
A little bit about myself