So I recently decided to give NixOS a proper shot, and honestly? The installation process was way more involved than I expected. But after getting everything set up with LUKS encryption, LVM for flexibility, and Btrfs subvolumes for snapshots… yeah, it was totally worth it.
I’m documenting this mainly for myself (because I’ll definitely forget these steps in like 3 months), but figured it might help someone else trying to do the same setup.
What we’re building here
The goal is a fully encrypted system with:
- LUKS encryption on the main partition
- LVM for managing volumes
- Btrfs with multiple subvolumes (including a read-only snapshot for easy rollbacks)
- 32GB swap
- Separate subvolumes for
/home,/nix,/persist, and/var/log
Here’s what the final partition layout looks like:
/dev/sda1 → 600MB EFI partition
/dev/sda2 → 1GB /boot (unencrypted, needed for bootloader)
/dev/sda3 → Everything else (LUKS encrypted)
└─ LVM volume group
├─ 32GB swap
└─ Root volume (Btrfs)
├─ @ (main root subvolume)
├─ @home
├─ @nix (nix store goes here)
├─ @persist (for persistent state)
├─ @log (/var/log)
└─ @fresh (read-only snapshot of @)
Step 1: Partitioning the disk
First things first, boot into the NixOS installer and get ready to partition /dev/sda (or whatever your disk is called).
bash
fdisk /dev/sda
In fdisk:
- Press
gto create a new GPT partition table - Create partition 1: press
n, then1, then+600M→ this is for EFI - Create partition 2: press
n, then2, then+1G→ this is for /boot - Create partition 3: press
n, then3, press Enter twice to use remaining space - Press
tand set partition 1 to type 1 (EFI System) - Press
wto write changes
Now format the first two partitions:
bash
mkfs.fat -F32 -n EFI /dev/sda1
mkfs.ext4 -L boot /dev/sda2
Step 2: Setting up LUKS encryption
This is where we encrypt the main partition. You’ll be prompted to enter a passphrase – make it good because you’ll need it every time you boot.
bash
cryptsetup luksFormat /dev/sda3 --label CRYPT
Type YES (in all caps) to confirm, then enter your passphrase twice.
Now open the encrypted partition:
bash
cryptsetup open /dev/sda3 crypt
Step 3: LVM setup
Time to set up LVM on top of the encrypted partition:
bash
pvcreate /dev/mapper/crypt
vgcreate vg /dev/mapper/crypt
Create the logical volumes – one for swap, one for everything else:
bash
lvcreate -L 32G -n swap vg
lvcreate -l '100%FREE' -n root vg
Format them:
bash
mkswap -L swap /dev/vg/swap
mkfs.btrfs -L nixos /dev/vg/root
Step 4: Btrfs subvolumes
This part is honestly pretty cool. We’re creating separate subvolumes so we can snapshot and manage different parts of the filesystem independently.
First, mount the root volume temporarily:
bash
mount /dev/vg/root /mnt
Create all the subvolumes:
bash
btrfs subvolume create /mnt/@
btrfs subvolume create /mnt/@home
btrfs subvolume create /mnt/@nix
btrfs subvolume create /mnt/@persist
btrfs subvolume create /mnt/@log
Here’s the clever bit – create a read-only snapshot of the root subvolume. This gives you a pristine copy you can use for rollbacks:
bash
btrfs subvolume snapshot -r /mnt/@ /mnt/@fresh
Unmount it now:
bash
umount /mnt
Step 5: Mounting everything properly
Now we mount all the subvolumes where they need to be. This is important – the order matters here.
bash
# Mount the main root subvolume first
mount -o compress=zstd,subvol=@ /dev/vg/root /mnt
# Create mount points
mkdir -p /mnt/{home,nix,persist,var/log,boot}
# Mount the other subvolumes
mount -o compress=zstd,subvol=@home /dev/vg/root /mnt/home
mount -o compress=zstd,noatime,subvol=@nix /dev/vg/root /mnt/nix
mount -o compress=zstd,subvol=@persist /dev/vg/root /mnt/persist
mount -o compress=zstd,subvol=@log /dev/vg/root /mnt/var/log
# Mount boot partitions
mount /dev/sda2 /mnt/boot
mkdir -p /mnt/boot/efi
mount /dev/sda1 /mnt/boot/efi
# Enable swap
swapon /dev/vg/swap
Note: I’m using compress=zstd on all Btrfs mounts because why not? Free compression with minimal CPU overhead. Also noatime on /nix to reduce writes.
Step 6: Generate NixOS config
Let NixOS scan your hardware and generate the initial config:
bash
nixos-generate-config --root /mnt
Now edit /mnt/etc/nixos/configuration.nix and /mnt/etc/nixos/hardware-configuration.nix. Here’s what mine look like:
configuration.nix:
configuration.nix:
nix
{ config, pkgs, ... }:
{
imports = [
./hardware-configuration.nix
];
# Bootloader setup for UEFI
boot.loader.systemd-boot.enable = true;
boot.loader.efi.efiSysMountPoint = "/boot/efi";
boot.loader.efi.canTouchEfiVariables = true;
boot.loader.grub.useOSProber = false;
# Docker with Btrfs
virtualisation.docker.enable = true;
virtualisation.docker.storageDriver = "btrfs";
virtualisation.docker.rootless = {
enable = true;
setSocketVariable = true;
};
networking.hostName = "nixos";
networking.networkmanager.enable = true;
time.timeZone = "Europe/Warsaw";
i18n.defaultLocale = "pl_PL.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "pl_PL.UTF-8";
LC_IDENTIFICATION = "pl_PL.UTF-8";
LC_MEASUREMENT = "pl_PL.UTF-8";
LC_MONETARY = "pl_PL.UTF-8";
LC_NAME = "pl_PL.UTF-8";
LC_NUMERIC = "pl_PL.UTF-8";
LC_PAPER = "pl_PL.UTF-8";
LC_TELEPHONE = "pl_PL.UTF-8";
LC_TIME = "pl_PL.UTF-8";
};
# KDE Plasma 6 desktop
services.xserver.enable = true;
services.displayManager.sddm.enable = true;
services.desktopManager.plasma6.enable = true;
services.xserver.xkb = {
layout = "pl";
variant = "";
};
# Audio with PipeWire
services.pulseaudio.enable = false;
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
jack.enable = true;
};
# Bluetooth
hardware.bluetooth.enable = true;
hardware.bluetooth.powerOnBoot = true;
users.users.your_user_name = {
isNormalUser = true;
description = "m8m";
extraGroups = [ "wheel" "networkmanager" "audio" "video" "docker" ];
hashedPassword = "your_hashed_password";
};
# Allow some unfree packages
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkg.pname or (builtins.parseDrvName pkg.name).name) [
"vscode"
];
environment.systemPackages = with pkgs; [
kdePackages.kate
kdePackages.ark
kdePackages.konsole
kdePackages.dolphin
kdePackages.spectacle
firefox
git
wget
vim
nodejs_24
vscode
docker
docker-compose
];
security.sudo.enable = true;
system.stateVersion = "25.05";
}
hardware-configuration.nix:
nix
{ config, lib, pkgs, modulesPath, ... }:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usb_storage" "usbhid" "sd_mod" ];
boot.initrd.kernelModules = [ "dm-snapshot" ];
boot.kernelModules = [ "kvm-amd" ];
boot.extraModulePackages = [ ];
# LUKS configuration - IMPORTANT: change the UUID to match your setup
boot.initrd.luks.devices = {
luksroot = {
device = "/dev/disk/by-uuid/your_uuid_disk";
preLVM = true;
allowDiscards = true;
};
};
# Filesystem mounts
fileSystems."/" = {
device = "/dev/vg/root";
fsType = "btrfs";
options = [ "subvol=@" "compress=zstd" ];
};
fileSystems."/home" = {
device = "/dev/vg/root";
fsType = "btrfs";
options = [ "subvol=@home" "compress=zstd" ];
};
fileSystems."/nix" = {
device = "/dev/vg/root";
fsType = "btrfs";
options = [ "subvol=@nix" "compress=zstd" "noatime" ];
};
fileSystems."/persist" = {
device = "/dev/vg/root";
fsType = "btrfs";
options = [ "subvol=@persist" "compress=zstd" ];
};
fileSystems."/var/log" = {
device = "/dev/vg/root";
fsType = "btrfs";
options = [ "subvol=@log" "compress=zstd" ];
};
fileSystems."/boot" = {
device = "/dev/sda2";
fsType = "ext4";
};
fileSystems."/boot/efi" = {
device = "/dev/sda1";
fsType = "vfat";
options = [ "fmask=0077" "dmask=0077" ];
};
swapDevices = [
{ device = "/dev/vg/swap"; }
];
networking.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
Important: You need to find the UUID of your encrypted partition. Run blkid /dev/sda3 and copy the UUID into the boot.initrd.luks.devices.luksroot.device line in hardware-configuration.nix.
Step 7: Install
Cross your fingers and run:
bash
nixos-install
It’ll take a while to download and install everything. When it’s done, set a root password if you want (though I just rely on the user account being in the wheel group).
Reboot:
bash
reboot
On boot, you’ll be prompted for your LUKS passphrase, then you should see the login screen.
Final thoughts
This setup gives you a lot of flexibility. The Btrfs subvolumes mean you can snapshot before major system changes, and the @fresh read-only snapshot is basically a „factory reset” point you can always roll back to. The /persist subvolume is great if you want to experiment with impermanence setups later (where most of the system gets wiped on boot).
Is it overkill? Probably. But it’s also pretty neat having everything properly isolated and encrypted.
Let me know if you run into any issues with this guide – I might have missed something since I’m writing this from memory!