# 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 |