# Installation Guide This guide details the entire process of provisioning a NixOS system with: - Nix Flakes enabled by default, with all system configuration living in a Flake - root as tmpfs - impermanence persisting files in the root or per normal user - disko for declarative disk configuration - installation using NixOS-anywhere, allowing for advanced installations The intent is for this guide to act as the NixOS equivalent to the ArchWiki installation guide. # 1. Environment Preparation Let's start by preparing the target machine. ## On the source machine Download the latest NixOS ISO and burn it to a flash drive. ## On the target machine Boot the machine using the flash drive. Connect it to the internet. Open a terminal and run the following commands: ```bash # switch to the root user $ sudo su - root # set a temporary password for the root user, like "12345" $ passwd # show the current ip address, remember this for later $ ip addr ``` All steps after this point are to be executed from the **source machine**. ## Creating a Git repository This repository will house all the Nix configurations for all of our systems. ```bash # Create a temporary shell with some basic tools to get going $ nix-shell -p git gh openssh helix nil # create an ssh key pair for the source machine (if needed) $ < /dev/zero > /dev/null ssh-keygen -q -N "" -t ed25519 # Login to GitHub $ gh auth login # Create a repo for your flake, call it whatever you like $ gh repo create nixcfg --public --clone # Navigate inside the newly created Git repo directory $ cd nixcfg ``` ## Configuring the Flake ```bash $ hx flake.nix ``` ```nix { inputs = { nixpkgs = { url = "github:nixos/nixpkgs/nixos-23.11"; }; snowfall-lib = { url = "github:snowfallorg/lib"; inputs.nixpkgs.follows = "nixpkgs"; }; agenix = { url = "github:yaxitech/ragenix"; inputs.nixpkgs.follows = "nixpkgs"; }; disko = { url = "github:nix-community/disko"; inputs.nixpkgs.follows = "nixpkgs"; }; impermanence = { url = "github:nix-community/impermanence"; }; home-manager = { url = "github:nix-community/home-manager/release-23.11"; inputs.nixpkgs.follows = "nixpkgs"; }; nix-index-database = { url = "github:Mic92/nix-index-database"; inputs.nixpkgs.follows = "nixpkgs"; }; }; outputs = inputs: inputs.snowfall-lib.mkFlake { inherit inputs; src = ./.; snowfall.namespace = "nixcfg"; channels-config.allowUnfree = true; systems.modules.nixos = with inputs; [ agenix.nixosModules.default disko.nixosModules.disko impermanence.nixosModules.impermanence nix-index-database.nixosModules.nix-index home-manager.nixosModules.home-manager { # configure home manager to use this flake's revision of nixpkgs home-manager.useGlobalPkgs = true; home-manager.useUserPackages = true; # enable comma and nix index programs.command-not-found.enable = false; programs.nix-index-database.comma.enable = true; # populate nix channels with this flake's nixpkgs revision nix.nixPath = [ "nixpkgs=${inputs.nixpkgs}" ]; # set any nix settings you want here. for reference, see: # https://nixos.org/manual/nix/unstable/command-ref/conf-file.html#available-settings nix.settings = { # enable flakes experimental-features = [ "nix-command" "flakes" ]; }; } ]; }; } ``` ## Configuring the DevShell ```bash $ hx shells/default/default.nix ``` ```nix { pkgs, inputs, system, ... }: pkgs.mkShell { shellHook = '' export EDITOR=hx agenix() { command agenix --rules secrets/__secrets__.nix "$@" } decrypt() { agenix --editor cat --edit "$1" | grep -v "wasn't changed" } sysdecrypt() { decrypt "secrets/systems/$hostname/$1.age" } install-nixos() { temp=$(mktemp -d) cleanup() { rm -rf "$temp" } trap cleanup EXIT etc="$temp/persist/etc" ssh="$etc/ssh" mkdir -p "$ssh" && chmod 755 "$ssh" sysdecrypt ssh_host_ed25519_key > "$ssh/ssh_host_ed25519_key" chmod 600 "$ssh/ssh_host_ed25519_key" sysdecrypt machine-id > "$etc/machine-id" chmod 644 "$etc/machine-id" command nixos-anywhere "root@$ip" \ --flake ".#$hostname" \ --build-on-remote -L \ --extra-files "$temp" \ --disk-encryption-keys /tmp/luks.key <(sysdecrypt luks) } ''; packages = with pkgs; [ coreutils git gh openssh openssl rsync helix nil nixos-anywhere inputs.agenix.packages.${system}.default ]; } ``` ## Using the DevShell Our temporary `nix-shell` environment can be replaced with `nix develop` now. ```bash # track all files via Git, or the flake won't see them $ git add . # pin our flake dependencies $ nix flake lock && git add flake.lock # leave our temporary development environment $ exit # navigate back to nixcfg directory $ cd nixcfg # enter our improved development environment $ nix develop # set environment variables for later user # - replace <user> with your intended primary user's username # - replace <hostname> with your intended target system's hostname # - replace <ip> with the IP address noted earlier of your target system # - replace <arch> with the architecture of your target system (ex: x86_64-linux) export user="<user>" hostname="<hostname>" ip="<ip>" arch="<arch>" ``` ## Copying files from the target to the source machine There are certain files that we must retrieve from the target system in order to configure our Flake. These include the `hardware-configuration.nix` file, which contains configuration for the supported hardware of the system, the disk layout from `lsblk` for partitioning, and the `ssh` keys generated by the live installer ISO, which will be the only method of decrypting our secrets later on. ```bash # ssh in as root using temp password, then delete it in favor of ssh key $ ssh-keygen -R "$ip" && ssh-copy-id -i ~/.ssh/id_ed25519.pub "root@$ip" && ssh "root@$ip" $ passwd -d root # create nixos config $ nixos-generate-config --no-filesystems --root /mnt # copy disk layout $ lsblk > /mnt/etc/disks.txt # copy ssh key pair $ mkdir -p /mnt/etc/ssh && cp -r /etc/ssh/ssh_host_ed25519* /mnt/etc/ssh # copy machine id $ cp /etc/machine-id /mnt/etc # back to host machine $ exit # create a directory to store files from target machine temporarily $ mkdir mnt && echo mnt >> .gitignore # copy /mnt to the source machine $ rsync -e ssh -azvhP "root@$ip:/mnt/" ./mnt ``` # 2. Secrets Configuration ## Configuring agenix ```bash # create a directory for secrets for the target machine $ mkdir -p secrets/systems/$hostname/ $ hx secrets/__secrets__.nix ``` ```nix let # !cat mnt/etc/ssh/ssh_host_ed25519_key.pub | tr -d '\n' targetSystem = "<target system ssh public key>"; # !cat ~/.ssh/id_ed25519.pub | tr -d '\n' sourceSystem = "<source system ssh public key>"; allSystems = [ targetSystem sourceSystem ]; in { # replace <hostname> "systems/<hostname>/luks.age".publicKeys = allSystems; "systems/<hostname>/password.age".publicKeys = allSystems; "systems/<hostname>/machine-id.age".publicKeys = allSystems; "systems/<hostname>/ssh_host_ed25519_key.age".publicKeys = allSystems; } ``` ## Creating the disk decryption password ```bash $ agenix --edit secrets/systems/$hostname/luks.age ``` Type the password into the editor and save and quit. ## Creating a password for a user ```bash $ agenix --edit secrets/systems/$hostname/password.age ``` Once Helix has opened, do the following: - Type "i" to enter insert mode - Type your password (make sure not to add additional whitespace or newlines) - Press "Esc" to return to normal mode - Type "x" to select the entire line - Type "|" to run a command, passing the selection as stdin - Type "openssl passwd -6 -stdin | tr -d '\n'" - Press "Enter" to apply the command - Type ":wq!" to save and exit ## Creating the machine id secret ```bash $ agenix --edit secrets/systems/$hostname/machine-id.age ``` Once Helix has opened, do the following: - Type "!" to run a command - Type "cat mnt/etc/machine-id" - Press "Enter" to apply the command - Type ":wq!" to save and exit ## Creating the ssh_host_ed25519_key secret ```bash $ agenix --edit secrets/systems/$hostname/ssh_host_ed25519_key.age ``` Once Helix has opened, do the following: - Type "!" to run a command - Type "cat mnt/etc/ssh/ssh_host_ed25519_key" - Press "Enter" to apply the command - Type ":wq!" to save and exit # 3. Disk Configuration ## Configuring the disks ```bash # show the existing disk layout $ cat mnt/etc/disks.txt $ hx systems/$arch/$hostname/disk-configuration.nix ``` ```nix { config, inputs, ... }: let disk = "<disk>"; # ex: nvme0n1 root-size = "<root-size>"; # ex: 2G ; recommendation: 2G swap-size = "<swap-size>"; # ex: 8G ; recommendation: half of the RAM in { fileSystems."/persist".neededForBoot = true; disko.devices = { nodev."/" = { fsType = "tmpfs"; mountOptions = [ "defaults" "size=${root-size}" "mode=755" ]; }; disk."${disk}" = { device = "/dev/${disk}"; type = "disk"; content = { type = "gpt"; partitions = { ESP = { size = "512M"; type = "EF00"; content = { type = "filesystem"; format = "vfat"; mountpoint = "/boot"; }; }; luks = { size = "100%"; content = { type = "luks"; name = "crypted"; passwordFile = "/tmp/luks.key"; settings.allowDiscards = true; content = { type = "btrfs"; extraArgs = [ "-f" ]; subvolumes = { "/nix" = { mountpoint = "/nix"; mountOptions = [ "compress=zstd" "noatime" ]; }; "/persist" = { mountpoint = "/persist"; mountOptions = [ "compress=zstd" "noatime" ]; }; "/swap" = { mountpoint = "/swap"; swap.swapfile.size = "${swap-size}"; }; }; }; }; }; }; }; }; }; } ``` # 4. System Configuration ```bash $ hx systems/$arch/$hostname/default.nix ``` ```nix { config, inputs, pkgs, system, ... }: let user = "<user>"; hostname = "<hostname>"; layout = "<layout>"; # keyboard layout; ex: us locale = "<locale>"; # ex: en_US.UTF-8 timezone = "<timezone>"; # ex: America/New_York ssh-keys = [ # !cat ~/.ssh/id_ed25519.pub | tr -d '\n' "<source system ssh public key>" ]; in { imports = [ ./hardware-configuration.nix ./disk-configuration.nix ]; # linux kernel version to use boot.kernelPackages = pkgs.linuxPackages_latest; # ensure the system can boot boot.loader.systemd-boot.enable = true; boot.loader.efi.canTouchEfiVariables = true; # ensure the system can connect to the internet networking.hostName = hostname; networking.networkmanager.enable = true; # assign the machine id environment.etc.machine-id.source = "/persist/etc/machine-id"; # ensure the system can be accessed remotely via ssh services.openssh = { enable = true; # ragenix uses this to determine which ssh keys to use for decryption hostKeys = [{ path = "/persist/etc/ssh/ssh_host_ed25519_key"; type = "ed25519"; }]; }; # configure the keyboard layout services.xserver.xkb.layout = layout; console.keyMap = layout; # set the locale and timezone i18n.defaultLocale = locale; time.timeZone = timezone; # enable the GNOME desktop environment services.xserver = { enable = true; libinput.enable = true; desktopManager.gnome.enable = true; displayManager.gdm.enable = true; displayManager.autoLogin.user = user; }; # enable audio via pipewire sound.enable = false; hardware.pulseaudio.enable = false; security.rtkit.enable = true; services.pipewire = { enable = true; alsa.enable = true; alsa.support32Bit = true; pulse.enable = true; jack.enable = true; }; # configure standard fonts fonts = { fontDir.enable = true; packages = with pkgs; [ noto-fonts noto-fonts-emoji noto-fonts-cjk ]; }; # enable printer support services.printing.enable = true; # disable sudo password prompts security.sudo.wheelNeedsPassword = false; # add additional system packages to install environment.systemPackages = with pkgs; [ gh devbox ]; # secrets for this machine age.secrets = { password.file = ../../../secrets/systems/${hostname}/password.age; }; # configure the users of this system users.users = { root.hashedPasswordFile = config.age.secrets.password.path; "${user}" = { isNormalUser = true; hashedPasswordFile = config.age.secrets.password.path; extraGroups = [ "wheel" "networkmanager" ]; openssh.authorizedKeys.keys = ssh-keys; }; }; # configure persistent files via impermanence environment.persistence."/persist" = { hideMounts = true; directories = [ "/var/log" "/var/lib/bluetooth" "/var/lib/nixos" "/var/lib/systemd/coredump" "/etc/NetworkManager/system-connections" { directory = "/var/lib/colord"; user = "colord"; group = "colord"; mode = "u=rwx,g=rx,o="; } ]; files = []; users."${user}" = { directories = [ "Desktop" "Documents" "Downloads" "Music" "Pictures" "Projects" "Public" "Templates" "Videos" { directory = ".ssh"; mode = "0700"; } ".config/nixcfg" ".config/gh" ]; files = [ ".bash_history" ".config/monitors.xml" ]; }; }; # 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 = "23.11"; # Did you read the comment? } ``` # 5. Home Configuration ## Configuring home manager We have finished covering system configuration, but now we must configure our user account. The minimum config below sets up `home-manager` for `$user` on the given `$hostname`, and configures `git` with the minimum configuration. ```bash $ hx homes/$arch/$user@$hostname/default.nix ``` ```nix { ... }: { home.username = "<REPLACEME>"; # ex: john home.homeDirectory = "/home/<REPLACEME>"; # ex: /home/john home.stateVersion = "23.11"; programs.git = { enable = true; userName = "<REPLACEME>"; # ex: John Doe userEmail = "<REPLACEME>"; # ex: [email protected] extraConfig.init.defaultBranch = "main"; }; } ``` Replace all the `<REPLACEME>`s above and remove the comments as needed. # 6. System Deployment ## Deploying to the target system ```bash # move the hardware config next to the system config $ mv mnt/etc/nixos/hardware-configuration.nix systems/x86_64-linux/$hostname/ # add everything to Git and push $ git add . $ git commit -m "Created my first NixOS system flake" $ git push # install the system $ install-nixos ``` Grab some popcorn and take a seat, this may take a while, especially if your system requires certain packages to be compiled from source. ## Fixing up the target system There are a few one-time fixups that need to be done after a fresh install. ```bash # ssh in as the new user, replacing the old ssh information with the new one $ ssh-keygen -R "$ip" && ssh "$user@$ip" # switch to root user $ sudo su - root # generate a new ssh public key from the private key $ ssh-keygen -f /persist/etc/ssh/ssh_host_ed25519_key -y > /persist/etc/ssh/ssh_host_ed25519_key.pub $ chmod 644 /persist/etc/ssh/ssh_host_ed25519_key.pub # back to normal user $ exit # login to GitHub CLI $ gh auth login # clone the flake $ gh repo clone nixcfg ~/.config/nixcfg # close ssh session $ exit ``` ## Cleaning up the source system There should be minimal cleanup needed on the source system, as well. ```bash $ rm -rf mnt ``` # Other Resources ## Nix Projects | Source | Link | |---------------------|-----------------------------------------------------| | Determinate Systems | https://determinate.systems | | | https://zero-to-nix.com | | | https://github.com/DeterminateSystems/nix-installer | | | https://flakehub.com | | | https://flakehub.com/flake/DeterminateSystems/fh | | MyNixOS | https://mynixos.com | | Jetpack | https://jetpack.io/devbox | | | https://nixhub.io | | Cachix | https://cachix.org | | | https://devenv.sh | | Open Source Nix | https://github.com/Mic92/sops-nix | | | https://github.com/LnL7/nix-darwin | | | https://github.com/LGUG2Z/nixos-wsl-starter | | | https://nixos-and-flakes.thiscute.world | | Universal Blue | https://getfleek.dev | | Nix Community | https://github.com/nix-community/home-manager | | | https://github.com/nix-community/nixos-anywhere | | | https://github.com/nix-community/disko | | | https://github.com/nix-community/lanzaboote | | | https://github.com/nix-community/impermanence | | | https://github.com/nix-community/nixGL | | | https://github.com/nix-community/comma | | | https://github.com/nix-community/nix-index-database | | | https://github.com/nix-community/nix-direnv | | | https://github.com/nix-community/nixvim | | | https://github.com/nix-community/NixOS-WSL |