Configuring NixOS

Last modified:

Linux

There are a lot of Linux distributions to choose from. It can be important to keep in mind what you want to use it for. I have used many distributions. While I like Arch Linux, it requires time to set up. NixOS resolves that issue. Although for beginners, I would recommend something like OpenSUSE or EndeavourOS. OpenSUSE is very solid; I would recommend it for advanced users too. It is easy to install and set up.

NixOS

NixOS focusses on creating a reproducible system. It allows you to get the exact same system running elsewhere easily. Using NixOS is overkill for most people. The time investment might not always be worth it for casual users. If you often find yourself messing with computers or reinstalling then it has benefits.

A great feature is rollbacks, which is useful when things go wrong. It is ideal for Linux enthusiasts who want stable, repeatable configurations. However, the learning curve can be steep especially without guides. For most users, default system settings work fine. It is often better to focus on using the computer rather than constantly tweaking it.

Common Linux mistakes

Linux is often seen as something for computer enthusiasts. But it can be easy to use depending on which operating system you select and which needs you have. Many computer geeks like to tweak, and that can lead to issues (or improvements). For the casual user Windows is fine.

Some common mistakes:

  • Configuring zswap without a swap file.
  • Using irqbalance; Modern kernels have this behavior already. Although irqbalance can still be useful on servers.
  • Modifying kernel variables without understanding what they do. Custom kernels often already set certain variables.
  • Accidentally making the system unbootable by not having rollbacks, backups, or a recovery USB.
  • ...

A simple configuration

This simple NixOS configuration does not use flakes. It is often beneficial to understand the basic configuration first. Flakes do have benefits, and this article covers them, but not too deeply. The benefits do not always outweigh the cons. The basics of flakes are sufficient for my purposes.

For this example, I merged the hardware-configuration.nix file into my main configuration for simplicity, but it's usually cleaner to keep them separate. Note that Nix file syntax can change, so if the configuration doesn't work for you, feel free to reach out (excluding the hardware configuration).

This configuration has some issues, but the essentials function:

  • KWallet autounlock works only with an empty password.
  • KDE automount for drives does not function.
  • Drawing tablets may have issues under Wayland; however, my setup uses Wayland, and Krita runs smoothly. It will only improve.

Finding options

You can find options and packages on these websites:

# Edit this configuration file to define what should be installed on
# your system.  Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running ‘nixos-help’).

{ config, lib, pkgs, modulesPath, ... }:

{
  # START HARDWARE
  # Lenovo Legion T5-26AMR5 (but with added RAM)

  boot = {
    initrd = {
      availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usb_storage" "usbhid" "sd_mod" ];
    };
    kernelPackages = pkgs.linuxPackages_zen;
    kernelModules = [ "kvm-amd" ];
    # Blacklist nouveau to make sure we enforce the closed source nvidia driver
    # Ethernet can act oddly sometimes I blacklist it
    blacklistedKernelModules = [ "nouveau" ]; # "r8169" ];
    # mitigations=off dangerous - more performance, less security - not relevant for modern CPUs

    # Kernel parameters
    # Add mitigations=off if more performance, less security is wanted, not relevant for modern CPUs etc
    kernelParams = [
      # Note: zswap is enabled by default with the zen kernel
      # https://github.com/zen-kernel/zen-kernel/wiki/Detailed-Feature-List#ram
      "zswap.enabled=1" # Enable zwap, other zswap settings are inherited of zen kernel
      "quiet" # Clean boot screen, suppress most kernel messages
      "rd.systemd.show_status=auto" # Only show errors of systemd, ignore other output during boot
      "rd.udev.log_priority=3" # Only show errors of udev ignore other output during boot
      "amd_pstate=guide" # Amd stuff is for amd cpu
      "nowatchdog" # Normal users do not need watchdogs
      "nmi_watchdog=0" # See previous watchdog comment
    ];

    kernel.sysctl = {
      # DEFAULTS ARE FINE IF YOU DO NOT KNOW WHAT YOU ARE DOING
      # CHANGING IT CAN DO MORE WRONG THAN RIGHT - LEAVING SYSCTL ALONE IS FINE

      # Check the following links if you want to tweak this
      # https://github.com/tolgaerok/nixos-kde/blob/main/core/modules/system-tweaks/kernel-tweaks/64GB-SYSTEM/64GB-SYSTEM.nix
      # https://github.com/tolgaerok/nixos-kde/blob/main/core/modules/system-tweaks/storage-tweaks/SSD/SSD-tweak.nix

      # Nobara Tweaks
      "fs.aio-max-nr" = 1000000;
      "fs.inotify.max_user_watches" = 65536;
      "kernel.panic" = 5;
      "kernel.pid_max" = 131072;

      # SSD tweaks: Adjust settings for an SSD to optimize performance.
      "vm.dirty_background_ratio" = "40";
      "vm.dirty_expire_centisecs" = "3000";
      "vm.dirty_ratio" = "80";
      "vm.dirty_time" = "0";
      "vm.dirty_writeback_centisecs" = "300";
    };

    # Use tmpfs for /tmp
    tmp = {
      cleanOnBoot = true;
      useTmpfs = true;
    };
  };

  # Enable trim on SSD
  # Trim service can better than using discard as mount option (never use both)
  services.fstrim.enable = true;

  # Basic filesystems (ext4, it has much history and is well-tested)
  fileSystems."/" = {
    # Mount disk by label
    device = lib.mkForce "/dev/disk/by-label/NixOS";
    fsType = "ext4";
    # no access times to reduce writes but access time can be useful metadata,
    # use relatime if you are not sure noatime includes nodiratime
    # commit=600 - if you lose your power, you will lose as much as the latest 600 seconds of work, but it does cause less writes
    options = [ "rw" "noatime" "commit=600" ];
  };

  fileSystems."/boot" = {
    device = lib.mkForce "/dev/disk/by-label/BOOT";
    fsType = "vfat";
  };

  # Put run in tmpfs
  fileSystems."/run" = {
    device = "tmpfs";
    # The size is a limit on the size, and is not kept reserved explicity for tmpfs.
    options = [ "size=3G" ];
  };

  # Zswap requires a swapfile or partition to work correctly
  swapDevices = [{
    device = "/var/lib/swapfile";
    # Swap is used when your RAM is full. It shouldn't happen often,
    # but you will be thankful that you have it when it is needed.

    # RAM size (8 GB) + 2 GB
    size = (1024 * 8) + (1024 * 2);
  }];

  # Allow closed-source firmware
  hardware.enableRedistributableFirmware = true;

  # Enables DHCP on each ethernet and wireless interface. This is the recommended approach.
  networking.useDHCP = lib.mkForce true;

  nixpkgs.hostPlatform = "x86_64-linux";

  # AMD microcode
  hardware.cpu.amd.updateMicrocode = true;

  # My desktop has TPM but I don't seem to need it
  # security.tpm2.enable = true;

  # Bluetooth
  hardware.bluetooth.enable = true;
  hardware.bluetooth.powerOnBoot = false; # disable bluetooth on boot

  # Note to self, vulkan is on the GPU, do not add amd vulkan stuff
  # (I knew this, but somehow made that mistake)
  hardware.graphics = {
    enable = true; # enable opengl
    extraPackages = [
      pkgs.vaapiVdpau
      pkgs.nvidia-vaapi-driver
    ];
    enable32Bit = true;
  };

  # Load nvidia driver (for Xorg or Wayland, config is the same)
  services.xserver.videoDrivers = [ "nvidia" ];
  hardware.nvidia = {

    # Modesetting is required.
    modesetting.enable = true;

    # Nvidia power management. Experimental, and can cause sleep/suspend to fail.
    # Enable this if you have graphical corruption issues or application crashes after waking
    # up from sleep. This fixes it by saving the entire VRAM memory to /tmp/ instead
    # of just the bare essentials.
    powerManagement.enable = false;

    # Fine-grained power management. Turns off GPU when not in use.
    # Experimental and only works on modern Nvidia GPUs (Turing or newer).
    powerManagement.finegrained = false;

    # Use the NVidia open source kernel module (not to be confused with the
    # independent third-party "nouveau" open source driver).
    # Support is limited to the Turing and later architectures. Full list of
    # supported GPUs is at:
    # https://github.com/NVIDIA/open-gpu-kernel-modules#compatible-gpus
    # Only available from driver 515.43.04+
    # Currently alpha-quality/buggy, so false is currently the recommended setting.
    open = true;

    # Enable the Nvidia settings menu,
    # accessible via `nvidia-settings`.
    nvidiaSettings = true;

    # Optionally, you may need to select the appropriate driver version for your specific GPU.
    package = config.boot.kernelPackages.nvidiaPackages.stable;
  };

  # END HARDWARE

  # START CONFIG

  # Bootloader
  boot = {
    loader = {
      systemd-boot = {
        enable = true;
        configurationLimit = 5;
      };
      efi.canTouchEfiVariables = true;
    };
    # NTFS is useful if you want to mount windows drives
    supportedFilesystems = [ "ntfs" ];
  };

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

    # Cloudflare dns, I am not a big fan of them, but they are the fastest
    nameservers = [ "1.1.1.1" "1.0.0.1" ];

    stevenblack = {
      # Block ads and other things by using the hosts file
      enable = true;
      block = [ "fakenews" "gambling" "porn" ];
    };
  };

  systemd.services = {
    # Fwupdmgr is not in the path - use the full path
    firmware-update = {
      description = "Update firmware.";
      script = ''
        /run/current-system/sw/bin/fwupdmgr refresh --offline --assume-yes || true > /dev/null
        /run/current-system/sw/bin/fwupdmgr update --offline --assume-yes > /dev/null
      '';
      serviceConfig.Type = "oneshot"; # Run the script once
      wantedBy = [ "multi-user.target" ]; # Start after login
    };
  };

  # Set your time zone.
  time.timeZone = "Europe/Brussels";

  # Select internationalisation properties.
  i18n.defaultLocale = "en_US.UTF-8";

  # KDE
  services.displayManager = {
    autoLogin = {
      enable = true;
      user = "rmw";
    };
    # I am using wayland now
    # defaultSession = "plasmax11";
  };

  environment.variables = {
    # nvidia force closed source driver
    GBM_BACKEND = "nvidia-drm";
    LIBVA_DRIVER_NAME = "nvidia";
    __GLX_VENDOR_LIBRARY_NAME = "nvidia";
    # this envs are useful for electron wayland
    ELECTRON_OZONE_PLATFORM_HINT = "auto";
    # variable for qt (wayland with fallback to x11)
    QT_QPA_PLATFORM = "wayland;xcb";
    # set sessiontype
    XDG_SESSION_TYPE = "wayland";
  };

  services.desktopManager.plasma6.enable = true;
  # pam_wallet will attempt to unlock the user's default KDE wallet upon login.
  # If the user has no kdewallet, or the login password does not match their wallet password,
  # KDE will prompt separately after login. sddm is the displaymanager
  # kwallet-pam is not compatible with GnuPG keys, the KDE Wallet must use the standard blowfish encryption
  # The wallet must be named kdewallet (default name). It does not unlock any other wallet(s).
  # It may be needed to remove the default created wallet first, thus removing all stored entries.
  # If the kwallet Migration Assistant asks for a password after every login, rename or delete the ~/.kde4/share/apps/kwallet folder.
  # If nothing works you can set it to an empty password, I ended up doing this, as I use keepassxc for most passwords
  pam.services = {
    sddm.kwallet.enable = true;
    # It may not autologin without this because sddm hardcodes PAM to the login rules
    login.kwallet.enable = true;
  };

  environment.plasma6.excludePackages = with pkgs.kdePackages; [
    gwenview
    okular
    oxygen
    khelpcenter
    plasma-browser-integration
    print-manager

    baloo # Disables indexing!! Be careful!
  ];

  # Drawing tablet
  hardware.opentabletdriver.enable = true;

  # Configure keymap in X11
  # Disable the X11 windowing system (wayland is in use instead)
  services.xserver.enable = false; # true;

  libinput = {
    # Mouse
    enable = true;
    mouse.accelProfile = "flat"; # disable mouse acceleration
    touchpad.accelProfile = "flat"; # disable touchpad acceleration
  };

  nix = {
    optimise.automatic = true;
    gc = {
      automatic = true;
      dates = "weekly";
      options = "--delete-older-than 7d";
    };
    settings = {
      auto-optimise-store = true;
      max-jobs = "auto";
      substituters = [
        "https://nix-community.cachix.org"
        "https://cache.nixos.org/"
      ];
      trusted-public-keys = [
        "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
      ];
    };
  };

  # Don't install documentation I don't use it
  documentation.enable = false; # documentation of packages

  # Enable ananicy
  services.ananicy = {
    enable = true;
    package = pkgs.ananicy-cpp;
    rulesProvider = pkgs.ananicy-cpp;
    settings = {
      apply_nice = true;
    };
  };

  # update firmware
  # list of supported devices: https://fwupd.org/lvfs/devices/
  services.fwupd.enable = true;
  # Command to update: fwupdmgr refresh && fwupdmgr update

  # Earlyoom killer
  systemd.oomd.enable = false;
  services.earlyoom.enable = true;

  # Keep journal in memory, do not store on disk
  # To reduce the size and keep it on disk use services.journald.extraConfig
  # DANGEROUS if you have boot issues!!!
  services.journald.storage = "volatile";

  # I do not use coredumps, and disabling storing them is better for security
  systemd.coredump.extraConfig = ''
    Storage=none
    ProcessSizeMax=0
  '';

  # disable suspend, hibernate and sleep
  systemd.sleep.extraConfig = ''
    AllowSuspend=no
    AllowHibernation=no
    AllowHybridSleep=no
    AllowSuspendThenHibernate=no
  '';

  # Make nixos boot slightly faster by turning these off during boot
  systemd.services.NetworkManager-wait-online.enable = false;
  systemd.services.systemd-udev-settle.enable = false;

  # Schedulers from https://wiki.archlinux.org/title/improving_performance
  services.udev.extraRules = ''
    # HDD
    ACTION=="add|change", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="bfq"
    # SSD
    ACTION=="add|change", KERNEL=="sd[a-z]*|mmcblk[0-9]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="bfq"
    # NVMe SSD
    ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="none"
  '';

  # Enable sound with pipewire.
  sound.enable = true;
  hardware.pulseaudio.enable = false;
  security.rtkit.enable = true;
  services.pipewire = {
    enable = true;
    alsa.enable = true;
    alsa.support32Bit = true;
    pulse.enable = true;
  };

  # Define a user account. Don't forget to set a password with ‘passwd’.
  users.users.rmw = {
    initialPassword = "r@@t";
    isNormalUser = true;
    description = "Robin Wils";
    extraGroups = [ "networkmanager" "network" "wheel" "storage" "disk" ];
    packages = with pkgs; [
      # Make sure that every character can be displayed by adding this font
      noto-fonts

      kate # simple text editor
      krita # drawing program
      keepassxc # password database
      brave # browser
      vscodium # vscode
      htop # commandline performance monitor
      mpv # commandline video player
      bun # similar to npm, but different

      archiver # lots of archivers & unarchivers into one package
      # Archive syntax: arc archive [archive name] [input files...]
      # Extract syntax: arc unarchive [archive name] [destination]
      # list syntax: arc ls [archive name]

      # nix file formatter
      nixfmt-rfc-style

      # GAMING (I am not a big gamer, only for party games)
      r2modman # modmanager, using it for lethal company
      # steam package
      obs-studio # recording
      shotcut # edit recordings

      # Automount - is easier to configure with home manager more on this later
      udiskie

      # qt recommends this system package for wayland
      qt6.qtwayland
    ];
  };

  # List packages installed in system profile. To search, run:
  # $ nix search wget
  environment.systemPackages = with pkgs; [
    # Nano editor is installed by default on nixos.
    nixfmt-rfc-style # nix file formatter
    doas-sudo-shim # Replaces sudo/aliases with doas for compatibility
  ];

  services.emacs.defaultEditor = true;
  programs = {
    # xwayland is often useful in wayland
    xwayland.enable = true;
    kdeconnect.enable = true;
    steam = {
      enable = true;
      remotePlay.openFirewall = true; # Open ports in the firewall for Steam Remote Play
      dedicatedServer.openFirewall = true; # Open ports in the firewall for Source Dedicated Server
      # steam input in wayland - translate X11 input events to uinput events
      extest.enable = true;
    };
    git = {
      enable = true;
      config = {
        user.name = "Robin Wils";
        user.email = "mrwils@tutanota.com";
      };
    };
    ssh.startAgent = true;
  };

  # Allow unfree packages
  nixpkgs.config.allowUnfree = true;

  security = {
    # Doas instead of sudo (better for security). The doas-sudo-shim package is included in the system packages, which replaces sudo/aliases with doas for compatibility
    sudo.enable = false;
    doas = {
      enable = true;
      extraRules = [
        {
          users = [ "rmw" ];
          keepEnv = true;
          persist = true; # Only require password once
        }
      ];
    };
  };

  # This value determines the NixOS release from which the default
  # settings for stateful data, like file locations and database versions
  # on your system were taken. It‘s perfectly fine and recommended to leave
  # this value at the release version of the first install of this system.
  # Before changing this value read the documentation for this option
  # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
  system.stateVersion = "24.05"; # Did you read the comment?

  # END CONFIG
}

To build the system you can execute "sudo nixos-rebuild switch".

Home manager & flakes

Flakes and home manager enhance reproducibility and simplify managing personal settings. Using them does not have to be hard. The benefits are clear.

Flakes

One reason you may want to use flakes is to make the system more reproducable. Another reason is that many GitHub projects only provide a NixOS package in the flake format, and certain features are only available by using flakes.

Moving to flakes is easy, and does not have to be scary. You just need to make a simple flake.nix file, and modify nix in the configuration.nix file.

# /etc/nixos/flake.nix
{
  description = "flake for yourHostNameGoesHere";

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

  outputs =
  { nixpkgs, ... }@inputs:
  {
    nixosConfigurations.yourHostNameGoesHere = nixpkgs.lib.nixosSystem {
      modules = [ ./configuration.nix ];
    };
  };
}
# Update in /etc/nixos/configuration.nix
 nix = {
  extraOptions = ''
    experimental-features = nix-command flakes
  '';
};

Flakes only work if the files are tracked by Git. You do not need to commit anything. Just go to /etc/nixos and execute "git init && git add .". When you use flakes you do need to use a new command to build your system:

sudo nixos-rebuild --flake .#yourHostNameGoesHere switch

Channels and inputs

Without flakes, NixOS uses channels to manage updates. With flakes, a similar concept called inputs is used. Both channels and inputs need to be updated periodically, as they point to specific versions of the NixOS repository.

Packages are linked to directories within that repository, which allows them to receive updates even if your flake is not fully up to date. You can run the command nix flake update /etc/nixos to refresh the inputs. If you notice that a package cannot be found in your repository, it may be a good indication that it is time to use this command. Although this situation is quite rare.

Home manager

Home manager makes even more of your setup reproducible. It is focused on reproducing the dotfiles (configuration for specific programs) in your home. The basic packages do not require home manager, but people highly recommend it.

Enabling it is not difficult, and in my previous example without home manager you may have noticed that udiskie was not configured. Home manager has better options for it. The different options in home manager are a good reason to start using it.

To enable home manager we have to update our flake.nix and configuration.nix. We also have to add a home.nix file. After that you can rebuild the flake.

# /etc/nixos/flake.nix
{
  description = "flake for yourHostNameGoesHere";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    # Add home manager
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs =
  { nixpkgs, ... }@inputs:
  {
    nixosConfigurations.yourHostNameGoesHere = nixpkgs.lib.nixosSystem {
      modules = [
        ./configuration.nix
        # Add home manager
        inputs.home-manager.nixosModules.default
      ];
    };
  };
}
# /etc/nixos/configuration.nix

# Add inputs if it wasn't present yet
{ pkgs, lib, inputs, ...}

{
  # Add this part
  home-manager = {
    # extraSpecialArgs are only needed if you import more modules to home manager. This can be useful for modules like impermanence.
    extraSpecialArgs = { inherit inputs; };
    users.yourusername = import ./home.nix;
  };
}
# /etc/nixos/home.nix
{ pkgs, inputs, ... }:

{
  # Enable unfree packages (it is enabled in configuration.nix, but needs to be enabled per user too)
  nixpkgs.config.allowUnfree = true;

  home = {
    keyboard.layout = "us";

    # Packages for your user (remove these out of your configuration.nix)
    packages = with pkgs; [
      # -- Cli --
      archiver # lots of archivers & unarchivers into one package
      # Archive syntax: arc archive [archive name] [input files...]
      # Extract syntax: arc unarchive [archive name] [destination]
      # list syntax: arc ls [archive name]

      gdu # Disk usage analyzer
      htop # Commandline taskmanager
      lsd # Better ls
      fd # Better find

      # -- Development --
      bun # similar to npm, but different
      nodePackages_latest.pnpm # bun does not always work
      powershell # Can often be powerful
      vscodium # Visual studio code fork without telemetry

      # -- Gaming --
      prismlauncher # minecraft launcher & modmanager
      r2modman # modmanager, using it for lethal company
      xorg.libxcb # required for steam according to people, but steam works without it

      # -- Photography --
      ansel # If ansel does not work, I use darktable. This package sometimes breaks

      # -- Basic tools --
      noto-fonts # Every character can be displayed by adding this font
      keepassxc # Password manager (highly recommended)
      mpv # Video player

      krita # Drawing/painting application
      obs-studio # Screen recorder & live-streaming
      shotcut # Video editting
      feh # Image viewer (command-line)

      imagemagick # Image convert commands
      ffmpeg # Video convert commands

      # -- Wayland --
      # qt recommends this system package for wayland
      qt6.qtwayland
    ];
  };

  programs = {
    home-manager.enable = true;

    # -- Cli --
    bat.enable = true; # Better cat
    zoxide.enable = true; # Better cd (use z "your-path")

    bash = {
      enable = true;
      historySize = 5000;
      historyFileSize = 5000;
      historyIgnore = [ "htop" ];
      historyControl = [ "erasedups" ];
      shellOptions = [ "cmdhist" ];
      shellAliases = {
        cat = "bat";
        find = "fd";
        ls = "lsd -Sl";
        lsa = "lsd -Sla";
        less = "less -R";
      };
      bashrcExtra = ''
        # -- functions --
        function nixos-update-channels {
          nix flake update /etc/nixos
        }
        function nixos-upgrade {
          nixos-rebuild switch --use-remote-sudo --flake /etc/nixos#
        }
        function nixos-upgrade-no-switch {
          nixos-rebuild boot --use-remote-sudo --flake /etc/nixos#
        }
        function format-nix-config {
          find /etc/nixos/ -name '*.nix' -exec nixfmt {} \;
        }
      '';
    };

    git = {
      enable = true;
      userEmail = "mrwils@tutanota.com";
      userName = "Robin Wils";
    };
  };

  # Here is the part in which udiskie is enabled
  services = {
    kdeconnect.enable = true; # remote connect
    ssh-agent.enable = true; # Ssh agent (useful for git)
    udiskie.enable = true; # Automount - make sure your user is in the disk group
  };

  # https://nixos.wiki/wiki/FAQ/When_do_I_update_stateVersion
  home.stateVersion = "24.05";
}

Going more NixOS than NixOS

As mentioned NixOS is all about being reproducible. There is one thing missing though. Cached files and other cluttered data may remain on your disk every boot. Programs may set their own configurations without you knowing it. You could miss certain files when you make a backup. But this issue can be resolved!

While impermanence is an advanced topic, it is still worth considering. It does not have to be that difficult, but I do recommend making a backup of your system first. Impermanence is a tool you can use to wipe your system except for your configured directories every boot. It does this by putting your root system in RAM, and mounting directories which you save permanently. You do not need tons of RAM to use this, but additional RAM does help.

The benefits are that you keep a clean system and that making backups becomes incredibly easy. Maintaining impermanence and figuring out what to keep is a bit difficult, but they are working on fixing that.

The GitHub page of impermanence is clear. I suggest reading through it. We can migrate to impermanence from our previous configuration without losing data.

Impermanence

We have to update the flake again.

# /etc/nixos/flake.nix
{
  description = "flake for yourHostNameGoesHere";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    # Add impermanence
    impermanence = {
      url = "github:nix-community/impermanence";
    };
  };

  outputs =
  { nixpkgs, ... }@inputs:
  {
    nixosConfigurations.yourHostNameGoesHere = nixpkgs.lib.nixosSystem {
      modules = [
        ./configuration.nix
        inputs.home-manager.nixosModules.default
        # Add impermanence
        inputs.impermanence.nixosModules.impermanence
      ];
    };
  };
}
# Update your filesystems (in hardware-configuration.nix or configuration.nix). Using btrfs is also an option, see the github of impermanence.

# Remove use tmpfs for /tmp, as it will be on tmpfs anyway

# boot = {
#   Use tmpfs for /tmp
#   tmp = {
#     cleanOnBoot = true;
#     useTmpfs = true;
#   };
# };

# System on tmpfs (Impermanence)
fileSystems."/" = {
  fsType = "tmpfs";
  # The size is a limit on the size, and is not kept reserved explicity for tmpfs. I have 80G of RAM, 7% is 5.6G which is more than enough, 2G is probably enough already
  options = [
    "size=7%"
    "mode=755"
  ];
};

fileSystems."/boot" = {
  device = "/dev/disk/by-label/BOOT";
  fsType = "vfat";
};

fileSystems."/persist" = {
  device = "/dev/disk/by-label/NixOS";
  fsType = "ext4";
  neededForBoot = true; # Persist is required to boot!!
  options = [
    "rw"
    "noatime"
    "commit=600"
  ];
};

# Mount the nix folder of our persist (our only drive) to /nix instead of /persist/nix https://nixos.wiki/wiki/Filesystems
fileSystems."/nix" = {
  device = "/persist/nix";
  options = [ "bind" "noatime" ];
};

# Zswap requires a swapfile or partition to work correctly
swapDevices = [
  {
    device = "/persist/system/var/lib/swapfile";
    # Swap is used when your RAM is full. It shouldn't happen often, but you will be thankful that you have it when it is needed.

    # RAM size 8 GB + 2 GB (since I have enough storage space) - I now have more than 8GB RAM, but decided to not change swap
    size = (1024 * 8) + (1024 * 2);
  }
];

I decided to separate the configuration of the root files to persist in a different file for clarity. Although the configuration of files to persist for home is in the home.nix.

# /etc/nixos/configuration.nix
imports = [
  # If you have your hardware config in a separated file leave this line
  ./hardware-configuration.nix
  # Add the impermanence config
  ./impermanence-root.nix
];
# /etc/nixos/impermanence-root.nix
{ pkgs, lib, ... }:

{
  environment = {
    # Persist these files, all other files will be removed in home.nix there are additional files to persist configured
    persistence."/persist/system" = {
      hideMounts = true;
      directories = [
        # Cannot be created by impermanence (they have to exist on disk)
        {
          directory = "/var/lib/nixos";
          user = "root";
          group = "root";
          mode = "0755";
        }
        {
          directory = "/var/log";
          user = "root";
          group = "root";
          mode = "0755";
        }
        # Everything should be cleaned up by systemd-tmpfiles over time anyway
        {
          directory = "/var/lib/systemd";
          user = "root";
          group = "root";
          mode = "0755";
        }
        {
          directory = "/var/tmp";
          user = "root";
          group = "root";
          mode = "1777";
        }
        {
          directory = "/var/spool";
          user = "root";
          group = "root";
          mode = "0777";
        }

        # nix tmp dir for rebuilds, don't fill our tmpfs root with that
        {
          directory = "/var/cache/nix";
          user = "root";
          group = "root";
          mode = "u=rwx,g=rx,o=rx";
        }

        # NetworkManager
        {
          directory = "/etc/NetworkManager/system-connections";
          mode = "0700";
        }
        {
          directory = "/var/lib/NetworkManager";
          mode = "0755";
        }
        # Printing
        # { directory = "/var/lib/cups"; user = "root"; group = "root"; mode = "0755"; }
        # { directory = "/var/cache/cups"; user = "root"; group = "lp"; mode = "0770"; }
        "/var/lib/bluetooth" # bluetooth state
        "/etc/nixos" # Configs
        "/etc/rustic" # Backup script
        # "/var/lib/btrfs" # records fs scrubbing status: https://btrfs.readthedocs.io/en/latest/Scrub.html (although I currently do not use scrub)
        # persist fwupd files to cache metadata and schedule updates for boot
        "/var/lib/fwupd"
        "/var/cache/fwupd"
      ];
      files = [
        "/etc/adjtime" # Is meant to stay persistent on Linux
        "/etc/machine-id" # Is meant to stay persistent on Linux
      ];
    };
  };

  programs.fuse.userAllowOther = true; # Fuse is required for impermanence

  # persist passwords
  # https://github.com/nix-community/impermanence/issues/120
  #     system.activationScripts = {
  #   etc_shadow = ''
  #     [ -f "/etc/shadow" ] && cp /etc/shadow /persist/system/etc/shadow
  #     [ -f "/persist/system/etc/shadow" ] && cp /persist/system/etc/shadow /etc/shadow
  #   '';

  #   users.deps = ["etc_shadow"];
  # };

  # systemd.services."etc_shadow_persistence" = {
  #   enable = true;
  #   description = "Persist /etc/shadow on shutdown.";
  #   wantedBy = ["multi-user.target"];
  #   path = [pkgs.util-linux];
  #   unitConfig.defaultDependencies = true;
  #   serviceConfig = {
  #     Type = "oneshot";
  #     RemainAfterExit = true;
  #     # Service is stopped before shutdown
  #     ExecStop = pkgs.writeShellScript "persist_etc_shadow" ''
  #       mkdir --parents "/persist/system/etc"
  #       cp /etc/shadow "/persist/system/etc/shadow"
  #     '';
  #   };
}
# /etc/nixos/home.nix

{ pkgs, inputs, ... }:

{
  # Import impermanence
  imports = [ inputs.impermanence.nixosModules.home-manager.impermanence ];

  # Add to your home section (packages etc can remain there)
  home = {
    # IMPORTANT: Set files to keep every reboot (persistence)
    # KDE has tons of files https://nixos.wiki/wiki/Impermanence
    persistence."/persist/home/rmw" = {
      directories = [
        "Downloads"
        "Music"
        "Pictures"
        "Documents"
        "Videos"
        "Desktop"
        # cache
        ".cache/nix" # avoid unnecessary fetching
        ".cache/nvidia" # avoid unnecessary computation
        # KDE & KDE connect
        ".config/KDE"
        ".config/kde.org"
        ".config/kdedefaults"
        ".config/kdeconnect"
        ".local/share/kwalletd" # after a wallet with empty password is made, also save it
        # Brave
        ".config/BraveSoftware/Brave-Browser/Default/Local Storage"
        ".config/BraveSoftware/Brave-Browser/Default/Extensions"
        ".config/BraveSoftware/Brave-Browser/Default/BraveWallet"
        ".config/BraveSoftware/Brave-Browser/Default/Sessions"
        # Keepass
        ".cache/keepassxc"
        ".config/keepassxc"
        # r2modman - Likely can be reduced in what is kept
        ".config/r2modman"
        ".config/r2modmanPlus-local"
        # vesktop
        ".config/vesktop/settings"
        ".config/vesktop/sessionData"
        # VSCodium
        ".config/VSCodium/CachedProfilesData"
        ".config/VSCodium/CachedExtensionVSIXs"
        ".config/VSCodium/User"
        ".config/.vscode-oss"
        # emacs
        ".emacs.d"
        # steam
        ".local/share/Steam"
        # Pnpm
        ".local/share/pnpm"
        # Bun
        ".bun"
        # OBS
        ".config/obs-studio"
        # Zoxide cache
        ".local/share/zoxide/"
        # keys
        ".gnupg"
        ".local/share/keyrings"
        ".ssh"
      ];
      files = [
        ".bash_history"
        # KDE
        ".config/kdeglobals"
        ".config/klanguageoverridesrc"
        ".config/kscreenlockerrc"
        ".config/ksplashrc"
        ".config/ktimezonedrc"
        ".config/kwalletrc"
        ".config/mimeapps.list"
        ".config/plasma-localerc"
        ".config/plasmarc" # wallpaper
        ".config/kwinrc"
        ".config/baloofilerc" # indexing
        ".config/plasma-org.kde.plasma.desktop-appletsrc" # wallpaper
        # Codium
        ".config/VSCodium/User/settings.json"
        # Vesktop
        ".config/vesktop/settings.json"
        ".config/vesktop/state.json"
        # Keepass
        ".config/KeePassXCrc"
        ".cache/keepassxc/keepassxc.ini"
        # Krita
        ".config/kritadisplayrc"
        ".config/kritarc"
        ".local/share/krita"
        # Brave
        ".config/BraveSoftware/Brave-Browser/Default/Bookmarks"
        ".config/BraveSoftware/Brave-Browser/Default/Favicons"
        ".config/BraveSoftware/Brave-Browser/Default/Cookies"
        ".config/BraveSoftware/Brave-Browser/Default/Preferences"
        ".config/BraveSoftware/Brave-Browser/Default/Secure Preferences"
        ".config/BraveSoftware/Brave-Browser/Default/Local State"
        ".config/BraveSoftware/Brave-Browser/Default/Web Data"
        ".config/BraveSoftware/Brave-Browser/Default/Login Data"
        ".config/BraveSoftware/Brave-Browser/Default/Login Data For Account"
        # OpenTabletDriver
        ".config/OpenTabletDriver/settings.json"
        # Freetube
        ".config/FreeTube/settings.db"
        ".config/FreeTube/profiles.db"
      ];
      allowOther = true;
    };
  };
}

Before you rebuild

I highly recommend making a backup first.

You may want to add the following function to your bashrcExtra to check which files are in tmpfs. Those files won't be persisted. It can be difficult to figure out which files to persist but impermanence is working on a solution for that

function get-files-in-tmpfs {
  sudo fd --one-file-system --base-directory / --type f --hidden --exclude "{tmp,etc/passwd}";
}

Rebuilding won't work yet. Before you rebuild you require a system directory on your root. Within impermanence-root.nix you can see which files impermanence expects. Make sure they exists before you rebuild. If you have issues with booting you can still rollback. There is no danger in trying.

I made a script to make the migration easier. It copies the directories you need to system. Update the variables to your situation before running it. It also sets permissions which likely isn't needed, if you applied that section make sure to change the hardcoded user.

#!/bin/bash

# Function to copy directories and files
copy_items() {
    local SOURCE_DIR_PREFIX="$1"
    local TARGET_DIR_PREFIX="$2"
    local DIRS=("${!3}")
    local FILES=("${!4}")

    # Copy directories to each target directory
    for DIR in "${DIRS[@]}"; do
        # Construct the corresponding source directory path
        FULL_SOURCE_DIR="$SOURCE_DIR_PREFIX/$DIR"
        # Construct the full target directory path
        FULL_TARGET_DIR="$TARGET_DIR_PREFIX/$DIR"

        # Ensure the target directory exists
        mkdir -p "$FULL_TARGET_DIR"

        # Copy files from the source to the target directory
        if [ -d "$FULL_SOURCE_DIR" ]; then
            cp -rp "$FULL_SOURCE_DIR/"* "$FULL_TARGET_DIR/" || {
                echo "Failed to copy from $FULL_SOURCE_DIR to $FULL_TARGET_DIR"
            }
        fi
    done

    # Copy specific files to the target directory
    for FILE in "${FILES[@]}"; do
        # Construct the full source file path
        FULL_SOURCE_FILE="$SOURCE_DIR_PREFIX/$FILE"
        # Construct the target file path
        FULL_TARGET_FILE="$TARGET_DIR_PREFIX/$FILE"

        # Ensure the target directory exists
        mkdir -p "$(dirname "$FULL_TARGET_FILE")"

        # Copy the file if it exists
        if [ -f "$FULL_SOURCE_FILE" ]; then
            cp -p "$FULL_SOURCE_FILE" "$FULL_TARGET_FILE" || {
                echo "Failed to copy $FULL_SOURCE_FILE to $FULL_TARGET_FILE"
            }
        fi
    done
}

## /system

# Source directory for system
SOURCE_DIR_PREFIX="/"
TARGET_DIR_PREFIX="/system"

DIRS=(
    "var/lib/nixos"
    "var/log"
    "var/lib/systemd"
    "var/tmp"
    "var/spool"
    "/var/cache/nix"
    "/etc/NetworkManager/system-connections"
    "var/lib/NetworkManager"
    "var/lib/bluetooth" # bluetooth state
    "/etc/nixos"
    "/etc/rustic"
    "var/lib/fwupd"
    "var/cache/fwupd"
)

FILES=(
    "/etc/adjtime"      # Is meant to stay persistent on Linux
    "/etc/machine-id"   # Is meant to stay persistent on Linux
    "/var/lib/swapfile" # Swap file
)

# Call the function for system
copy_items "$SOURCE_DIR_PREFIX" "$TARGET_DIR_PREFIX" DIRS[@] FILES[@]

# Set the correct owner
chown -R rmw /home
chown -R root /system/var
chown -R root /system/etc

# Set permissions for /var/log
chmod 750 /system/var/log
chmod 755 /system/var/lib
chmod 1777 /system/var/tmp
chmod 755 /system/etc

If you decided to use my aliases for nixos-upgrade be careful. We are changing filesystems here, so we need to call nixos-upgrade-no-switch instead. In fact, you always need to call that one on impermanence. Switching immediately does not seem to work on it. You need to reboot manually after updating the system.

# Use boot instead of switch!
nixos-rebuild boot --use-remote-sudo --flake /etc/nixos#

After your build

There are some unnecessary files left over of our previous config. We only required /system, /boot, /nix and /home now. So you can remove all other directories on the root.

# Remove all directories on root except the required ones
sudo find / -mindepth 1 -maxdepth 1 -type d ! -name 'system' ! -name 'boot' ! -name 'nix' ! -name 'home' -exec rm -rf {} +

NixOS config

You can find my NixOS configuration on Git. I do use impermanence. But reading other people's configurations can be useful even if you do not use flakes, home-manager or impermanence. It gives you an idea of available options.

GitLab (NixOS-config)

Share

Diaspora X Facebook LinkedIn

Donate