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!