Installing NixOS with Full Disk Encryption, LVM, and Btrfs Subvolumes

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 g to create a new GPT partition table
  • Create partition 1: press n, then 1, then +600M → this is for EFI
  • Create partition 2: press n, then 2, then +1G → this is for /boot
  • Create partition 3: press n, then 3, press Enter twice to use remaining space
  • Press t and set partition 1 to type 1 (EFI System)
  • Press w to 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!

Install Gentoo with UEFI, LUKS, LVM

In this article I’ll describe how you can install Linux Gentoo with Luks encryption, LVM and for PC with UEFI.

The point of this article is fact that Linux supports some cryptographic techniques to protect data on the hard disk (also whole partitions). So why not just use them?

Luks (Linux Unified Key Setup) is a way to allow you encrypt/decrypt specific disc and it was originally intended for Linux OS. Dm-crypt (DMCrypt kernel module) is used for proper operation as a kernel to handle encryption/decryption on the block disc level.

Luks is independent of any linux distribution, and any new kernel should be compatible. All sensitive and necessary data is stored in the partition header, and this allows to move data between devices.

To install, crypt or decrypt data we must using CLI commands.

Make sure you have an efi system

ls /sys/firmware/efi

If you see something, your machine ha an efi system


Wifi connection

Check all interfaces

ifconfig

Store wifi name and password in config file (wpa_supplicant.conf)

wpa_passphrase [ssid] ["passphrase"] > /etc/wpa_supplicant/wpa_supplicant.conf
wpa_supplicant -i [interface] -c /etc/wpa_supplicant/wpa_supplicant.conf

Checking broadcast connection

ping -c 3 8.8.8.8

Disk partition

parted -a optimal /dev/sda
    > rm [number]

When number is number of partition

    > mklabel gpt
    > unit mib
    > mkpart primary 1 513
    > name 1 boot
    > set 1 boot on
    > mkpart primary 513 -1
    > name 2 lvm
    > set 2 lvm on
    > print
    > quit

Create a filesystem

Before using the drive, we must format volume. We can use one of the different systems, like ext3, ext4, btrfs or xfs and so on.

mkfs.fat -F32 /dev/sda1

LVM

modprobe dm-crypt
/etc/init.d/lvmetad restart
cryptsetup -v -y -c aes-xts-plain64 -s 512 -hash sha512 --iter-time 5000 --use-random luksFormat /dev/sda2

or

cryptsetup -v -c serpent-xts-plain64 -s 512 --hash whirlpool --iter-time 5000 --use-random luksFormat /dev/sda2
ls /dev/mapper
cryptsetup luksDump /dev/sda2
cryptsetup luksOpen /dev/sda2 gentoo
lvmdiskscan

Create the physical volume

pvcreate /dev/mapper/gentoo
pvdisplay

Create the volume group

vgcreate gentoo /dev/mapper/gentoo
vgdisplay
lvmdiskscan
lvcreate -C y -L 32G gentoo -n swap

where 32G is the total size of the oparating memory

lvcreate -L 32G gentoo -n root
lvcreate -L 65G gentoo -n var
lvcreate -l +100%FREE gentoo -n home
lvdisplay
vgscan
vgchange -ay
mkswap /dev/mapper/gentoo-swap
mkfs.ext4 /dev/mapper/gentoo-root
mkfs.ext4 /dev/mapper/gentoo-var
mkfs.ext4 /dev/mapper/gentoo-home
swapon /dev/mapper/gentoo-swap
mount /dev/mapper/gentoo-root /mnt/gentoo
mkdir /mnt/gentoo/boot
mkdir /mnt/gentoo/home
mkdir /mnt/gentoo/var
mount /dev/sda1 /mnt/gentoo/boot
mount /dev/mapper/gentoo-var /mnt/gentoo/var
mount /dev/mapper/gentoo-home /mnt/gentoo/home
lsblk /dev/sda

Download stage3

cd /mnt/gentoo
links https://www.gentoo.org/downloads/mirrors/
ls -la

Extract stage3

tar xpvf stage3-* --xattrs-include='*.*' --numeric-owner

Config make.conf

nano -w /mnt/gentoo/etc/portage/make.conf
mirrorselect -i -o >> /mnt/gentoo/etc/portage/make.conf

Mount system

mkdir --parents /mnt/gentoo/etc/portage/repos.conf
cp /mnt/gentoo/usr/share/portage/config/repos.conf /mnt/gentoo/etc/portage/repos.conf/gentoo.conf
cp --dereference /etc/resolv.conf /mnt/gentoo/etc/
mount --types proc /proc /mnt/gentoo/proc
mount --rbind /sys /mnt/gentoo/sys
mount --rbind /dev /mnt/gentoo/dev
mount --make-rslave /mnt/gentoo/sys
mount --make-rslave /mnt/gentoo/dev
test -L /dev/shm && rm /dev/shm && mkdir /dev/shm
mount --types tmpfs --options nosuid,nodev,noexec shm /dev/shm
chmod 1777 /dev/shm
mkdir /mnt/gentoo/hostrun
mount --bind /run /mnt/gentoo/hostrun/

Mount bash and chroot

chroot /mnt/gentoo /bin/bash
source /etc/profile
export PS1="(chroot) $PS1"

Updating the ebuild repository

emerge-webrsync
emerge --sync

Edit /etc/portage

cd /etc/portage/
mkdir -p /etc/portage/package.{accept_keywords,license,mask,unmask,use}

Install Vim

echo "app-editors/vim lua luajit perl python ruby terminal vim-pager" > package.use/vim
emerge -av vim eix

Update the world

emerge --ask --verbose --update --deep --with-bdeps=y --newuse  --keep-going --backtrack=30 @world

Set new gcc version

gcc-config --list-profiles
gcc-config [version]
source /etc/profile
export PS1="(chroot) $PS1"

Re-emerge the libtool

emerge --ask --oneshot --usepkg=n sys-devel/libtool

Enable cpu features and update make.conf

emerge -av cpuid2cpuflags
cpuid2cpuflags >> /etc/portage/make.conf

Set timezone

echo "America/Los_Angeles" > /etc/timezone
emerge --config sys-libs/timezone-data
nano /etc/locale.gen
locale-gen
eselect locale list
eselect locale set [number]
env-update && source /etc/profile && export PS1="(chroot) ${PS1}"
eselect profile list
eselect profile set [number]
emerge -av gentoo-sources genkernel-next cryptsetup lvm2 linux-firmware

Edit /etc/fstab

nano /etc/fstab

This is the simple scheme used in this article. Remember that in your case the file /etc/fstab should be similar, unless you want to do it differently and you are sure about it.

/dev/sda1/bootvfatnoatime0 2
/dev/mapper/gentoo-root/ext4rw,relatime,data=ordered0 1
/dev/mapper/gentoo-home/homeext4rw,relatime,data=ordered0 2
/dev/mapper/gentoo-var/varext4rw,relatime,data=ordered0 2
/dev/mapper/gentoo-swapnoneswapdefaults0 0

Manual kernel configuration

cd /usr/src
ls -la
cd linux/
ln -s /usr/src/linux* /usr/src/linux

Edit genkernel.conf

nano /etc/genkernel.conf

Change LVM and LUKS to yes

LVM=”yes”

LUKS=”yes”

MAKEOPTS=”$(portageq envvar MAKEOPTS)”

genkernel --makeopts=-j17 --menuconfig --lvm --luks --no-zfs all

when 17 is number of processors + 1

nano /etc/lvm/lvm.conf

devices {
multipath_component_detection = 0
md_component_detection = 0
}

activation {
udev_sync = 0
udev_rules = 0
}

genkernel --lvm --luks initramfs
echo "sys-boot/grub mount device-mapper" > /etc/portage/package.use/grub
emerge -av grub gentoolkit 
nano /etc/default/grub

GRUB_CMDLINE_LINUX=”crypt_root=/dev/sda2 root=/dev/mapper/gentoo-root rootfstype=ext4 dolvm quiet”

grub-install --target=x86_64-efi --efi-directory=/boot /dev/sda --bootloader-id="Gentoo linux [GRUB]" --recheck
grub-mkconfig -o /boot/grub/grub.cfg

Change root password and create new user

passwd
useradd -m -G users,wheel,audio,video -s /bin/bash [user]
passwd [user]

Set hostname

echo "[hostname]" > /etc/hostname
nano /etc/hosts
127.0.0.1    [hostname].localdomain    localhost
emerge -av syslog-ng cronie mlocate
rc-update add syslog-ng default
rc-update add cronie default
rc-update add sshd default
rc-update add wpa_supplicant boot
rc-update add lvm boot

Configure networking

Desktop

emerge -av net-misc/dhcpcd

Laptop

emerge -av wireless-tools net-tools app-text/tree wpa_supplicant networkmanager
emerge -av x11-misc/xdotool x11-misc/wmctrl
rc-update add NetworkManager default
tree /sys/class/net

zgrep 'IWLWIFI\|IWLDVM\|IWLMVM' /proc/config.gz
    * iwlwifi
    M iwldvm
    M iwlmvm

exit
cd

Rebooting

exit
shutdown -r now

Aditional

Create LUKS header backup

To create LUKS header backup, we can use command:

cryptsetup luksHeaderBackup /dev/sda2 --header-backup-file sda2-luks-header-backup