I’ve been using NixOS as my main operating system for quite some time now.

As I recently upgraded my computer equipment by acquiring a new laptop, I decided that it’s time to invest some more time into further capitalizing on the features of NixOS. The main purpose of this post is to collect all the things I learned while doing so in a single place so that I’ll always be able to look it up again if need be.

Overview

In NixOS, the whole system configuration takes place in a single file, /etc/nixos/configuration.nix:

  { config, pkgs, ... } :

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

    ...

    # set timezone to Berlin
    time.timeZone = "Europe/Berlin";

    ...

    # set up user "tim"
    users.users.tim = {
      isNormalUser = true;
      ...
    };

    # install firefox
    environment.systemPackages = [ pkgs.firefox ];
  }

I want to use a single repository of customized nix configurations to set up both my laptop as well as my desktop as easily as possible.

As I usually use my laptop and my desktop for similar but not exactly identical tasks, I want the configuration to be as modular as possible: For example, there should be a module enabling and setting up LaTeX exactly as I use it: texlive with this and that package, my favorite text editor vim set up with additional plugins to support LaTeX syntax highlighting et cetera.

If I now plan to use LaTeX on a computer, I essentially just want to add something like

  custom.latex.enable = true;

in my configuration.nix to have my LaTeX tools installed and customized exactly like I want them to be.

For practical as well aesthetical reasons, I’d like this configuration to be user-specific whenever possible: I don’t want to configure vim globally on the whole machine, but just the user I’m writing LaTeX code on. (What if someone else would want to write some LaTeX on my laptop?) Turns out the home-manager project is adding tons of user-specific configuration options to the NixOS ecosystem.

To keep the configurations of my laptop and my desktop in sync with each other, I want to store my configuration in some git repository (added benefit: version control). This way, whenever I for example add a vim macro for editing LaTeX files, I’ll only have to commit my changes to the git repository and execute (something similar to)

git pull origin master
sudo nixos-rebuild switch

on my desktop to have the exact same configuration applied on my desktop as well.

My current setup

As both NixOS as well as home-manager still often lack proper documentation, I ended up mostly reading other people’s configuration files and stealing things from there.

Since it seems like the next major update of the nix package manager will introduce nix flakes, I decided to set up my system configuration using flakes as a (fairly stable) experimental feature. My current flake.nix file looks as follows:

{
  description = "tim's personal flake";

  inputs = rec {

    nixpkgs = {
      url = "github:NixOS/nixpkgs/nixos-21.05";
    };

    unstable = {
      url = "github:NixOS/nixpkgs/nixos-unstable";
    };

    home-manager = {
      url = "github:nix-community/home-manager/release-21.05";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    nixos-hardware = {
      url = "github:NixOS/nixos-hardware";
    };

    nur = {
      url = "github:nix-community/NUR";
    };

  };

  outputs = {self, nixpkgs, unstable, home-manager, nixos-hardware, nur} :
  let
    lib = nixpkgs.lib;
  in
  {

    nixosConfigurations = {

      thinkpad = lib.nixosSystem {
        system = "x86_64-linux";
        modules = [

          nixos-hardware.nixosModules.lenovo-thinkpad-t14s-amd-gen1

          home-manager.nixosModules.home-manager

          ./machines/thinkpad/configuration.nix
        ];
      };

    };

    thinkpad = self.nixosConfigurations.thinkpad.config.system.build.toplevel;
  };
}

The inputs record consists of all repositories that would otherwise be manually added to the available nix-channels. In my case, I usually use the most recent stable version of NixOS and its package tree nixpkgs. In case I need some test features (like flakes!), I have the unstable input source pointing to the current nixos-unstable git repository. home-manager also comes with its own set of nix expressions, nixos-hardware has some hardware-specific nix expressions that come in handy for my laptop and finally nur is the Nix User Repository.

Then I declare one configuration for my laptop thinkpad that uses the laptop-specific nix expressions of nixos-hardware, enables home-manager and refers to a configuration.nix file that I specifically prepared for this machine with it’s hardware-specific like partitioning etc.

This configuration file also includes all the remaining configuration I want to use.

The rough setup is as follows:

tims-flake/
├── flake.lock
├── flake.nix
├── machines/
│   ├── defaultMachine.nix
│   └── thinkpad/
│       ├── configuration.nix
│       ├── hardware-configuration.nix
│       └── thinkpad.nix
├── modules/
│   ├── custom/
│   │   ├── hm/
│   │   │   ├── default.nix
│   │   │   ├── dev/
│   │   │   │   ├── nix/
│   │   │   │   │   └── nix.nix
│   │   │   │   └── latex/
│   │   │   │       ├── latex.nix
│   │   │   │       └── config/
│   │   │   │           └── tex.vim
│   │   │   └── media/
│   │   │       └── ...
│   │   └── nixos/
│   │       ├── sys/
│   │       │   ├── ...
│   │       │   └── ...
│   │       ├── wm/
│   │       │   ├── x11/
│   │       │   │   └── x11.nix
│   │       │   └── gnome/
│   │       │       └── gnome.nix
│   │       └── default.nix
│   └── default.nix
├── profiles/
│   ├── default.nix
│   ├── development.nix
│   ├── multimedia.nix
│   └── research.nix
└── users/
    └── tim/
        ├── base.nix
        ├── default.nix
        └── networks/
            └── eduroam.nix

With this general setup, I now implement the system configuration as I see fit. For example, here’s the content of the latex.nix file of above:

### HOME MANAGER MODULE
{ config, lib, pkgs, ... } :

with lib;

let cfg = config.custom.hm.dev.latex;

in

{

  ### interface
  options = {

    custom.hm.dev.latex.enable     = mkEnableOption "config for latex";

    custom.hm.dev.latex.vim.enable = mkEnableOption "enable vim with latex plugins";

  };

  ### implementation
  config = mkMerge [

    (mkIf cfg.enable {

      home.packages = with pkgs; [

        # install texlive with all available packages
        texlive.combined.scheme-full

        # install the zathura pdf viewer (vim keybindings)
        zathura

        # also install texstudio
        texstudio
      ];
    })

    (mkIf cfg.vim.enable {

      programs.vim = {

        enable = true;

        plugins = with pkgs.vimPlugins; [
          vimtex
          ultisnips
          YouCompleteMe
        ];

        # enable custom settings for each filetype
        extraConfig =
        ''
        filetype plugin on
        '';
      };

      # use custom settings for .tex files
      home.file = {

        ".vim/ftplugin/tex.vim".source = ./config/tex.vim;

      };
    })
  ];
}

Workflow

Since I’m using nix flakes, I need to change the update procedure a little bit:

sudo nix flake update .

git add -A
git commit -m "flake update."
git push origin master

sudo nixos-rebuild --switch --flake .

First, I update the flake, then I upload the changes to its git repository and then I tell NixOS to rebuild itself using the updated flake.