Declarative Disk Partitioning with Disko

Updated

September 12, 2026

Declarative Disk Partitioning with Disko

fdisk notes in a wiki are a snowflake. The boring default is: Disko describes GPT, LUKS, and filesystems in Nix; you format from that file; nixos-anywhere uses the same file on a remote disk.

The Installing NixOS chapter partitioned a VM by hand so you would see mounts. From here on, the layout lives in git.

Mental model

Disko (github:nix-community/disko) is a NixOS module plus a script.

Mode What it does
format Partition, LUKS, mkfs. Destroys the disk.
mount Mounts in dependency order under /mnt
NixOS module Also emits fileSystems.* so you may not need a separate hardware file for mounts
disk-config.nix
      │
      ├── nixos-rebuild  →  fileSystems."/" etc.
      └── disko script   →  wipe /dev/disk/by-id/…  (once)

Device names (/dev/sda, /dev/vda, /dev/nvme0n1) move. Prefer /dev/disk/by-id/… on metal (ls -l /dev/disk/by-id/nvme-eui.* or ata-*, not wwn- aliases you do not understand). On a throwaway QEMU disk, /dev/vda is acceptable if the VM has one disk.

disko.devices.disk.main.device must match this machine. A shared module with device = "/dev/nvme0n1" on a fleet is how you wipe the wrong box. Per-host disk-config.nix, shared shape (ESP size, LUKS, Btrfs @persist).

nixos-anywhere SSHes into rescue, kexecs NixOS, runs Disko, copies the closure, installs the bootloader, reboots. Same disk-config.nix.

Worked examples

Case 1: Lab VM — one disk, no LUKS

Save as disk-config.nix:

# disk-config.nix
{
  disko.devices.disk.main = {
    type = "disk";
    device = "/dev/vda";
    content = {
      type = "gpt";
      partitions = {
        ESP = {
          size = "1G";
          type = "EF00";
          content = {
            type = "filesystem";
            format = "vfat";
            mountpoint = "/boot";
            mountOptions = [ "umask=0077" ];
          };
        };
        root = {
          size = "100%";
          content = {
            type = "filesystem";
            format = "ext4";
            mountpoint = "/";
          };
        };
      };
    };
  };
}

Import it from the host:

# configuration.nix fragment
{ inputs, ... }:
{
  imports = [
    inputs.disko.nixosModules.disko
    ./disk-config.nix
  ];
}

On a throwaway VM that is allowed to lose /dev/vda:

sudo nix --experimental-features 'nix-command flakes' run github:nix-community/disko -- \
  --mode disko ./disk-config.nix

--mode disko formats and mounts. There is no undo. Not your laptop’s NVMe.

Case 2: Metal — by-id + LUKS + Btrfs subvolumes

Save as disk-luks-btrfs.nix:

# disk-luks-btrfs.nix
{
  disko.devices.disk.main = {
    type = "disk";
    device = "/dev/disk/by-id/nvme-VENDOR_MODEL_SERIAL";
    content = {
      type = "gpt";
      partitions = {
        ESP = {
          size = "1G";
          type = "EF00";
          content = {
            type = "filesystem";
            format = "vfat";
            mountpoint = "/boot";
            mountOptions = [ "umask=0077" ];
          };
        };
        luks = {
          size = "100%";
          content = {
            type = "luks";
            name = "crypted";
            settings.allowDiscards = true;
            passwordFile = "/tmp/luks.key";
            content = {
              type = "btrfs";
              extraArgs = [ "-f" ];
              subvolumes = {
                "@root" = {
                  mountpoint = "/";
                  mountOptions = [ "compress=zstd" "noatime" ];
                };
                "@nix" = {
                  mountpoint = "/nix";
                  mountOptions = [ "compress=zstd" "noatime" ];
                };
                "@persist" = {
                  mountpoint = "/persist";
                  mountOptions = [ "compress=zstd" "noatime" ];
                };
                "@swap" = {
                  mountpoint = "/swap";
                  swap.swapfile.size = "8G";
                };
              };
            };
          };
        };
      };
    };
  };
}

ls -l /dev/disk/by-id/ on the machine (or rescue) and paste the id that is not a -partN partition. passwordFile is for provisioning; after install, unlock is passphrase or TPM (later hardening). Do not commit /tmp/luks.key.

@persist is the impermanence contract. Create it even if you still boot a persistent root today.

Case 3: Flake input

# flake.nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
    disko = {
      url = "github:nix-community/disko";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { self, nixpkgs, disko }: {
    nixosConfigurations.desk-server = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      specialArgs = { inherit disko; };
      modules = [
        disko.nixosModules.disko
        ./disk-config.nix
        ./configuration.nix
      ];
    };
  };
}
nix eval .#nixosConfigurations.desk-server.config.fileSystems.\"/\".device

You should see a mapper or UUID-style device coming from Disko, not a guess.

Case 4: nixos-anywhere

nix run github:nix-community/nixos-anywhere -- \
  --flake .#desk-server \
  --generate-hardware-config nixos-generate-config ./hardware-configuration.nix \
  root@203.0.113.42

Output (shape):

[nixos-anywhere] Connected to rescue over SSH.
[nixos-anywhere] kexec NixOS installer...
[nixos-anywhere] Running Disko on /dev/disk/by-id/…
[nixos-anywhere] Copying closure...
[nixos-anywhere] Installing bootloader...
[nixos-anywhere] Rebooting.

The remote must be a rescue environment you are allowed to wipe. Hetzner rescue, a cloud “reinstall,” or a live ISO with SSH. Not root@production-that-has-data.

Case 5: Parameterise the device per host

Save as disk-config.nix:

# disk-config.nix
{ diskDevice ? "/dev/vda", ... }:

{
  disko.devices.disk.main.device = diskDevice;
  # … rest of the layout using `main` …
}

Or two files: hosts/desk-vm/disk.nix (/dev/vda) and hosts/desk-metal/disk.nix (by-id). Same subvolume names so impermanence and backups stay identical.

The trap

The trap is pointing Disko at the wrong disk because /dev/nvme0n1 was the installer USB on this firmware. by-id includes the serial. Read lsblk -o NAME,SIZE,MODEL,SERIAL twice.

The other trap is committing passwordFile = "/tmp/luks.key" contents into the flake, or leaving a LUKS passphrase in settings.keyFile as a Nix string. Same store leak as any secret.

A third trap: running --mode format on a machine that already has /persist data. Disko is an installer, not a resize tool. Back up, then format.

The boring rule

  • Disk layout in git. Format from that file. Do not keep a parallel fdisk runbook.
  • by-id on real disks. /dev/vda only on single-disk lab VMs.
  • LUKS passphrase / key file is provisioning state, not a Nix string.
  • Same Disko file for nixos-anywhere and for fileSystems in the running config.
  • @nix and @persist as separate subvolumes before you turn on impermanence.

Try this

  1. On a disposable QEMU VM, apply Case 1. findmnt / /boot after Disko mount. Confirm /dev/vda1 is vfat and /dev/vda2 is ext4 (names may vary).
  2. ls -l /dev/disk/by-id on any Linux box and pick the id you would use. Do not run Disko there.
  3. Eval config.fileSystems from a flake that imports Disko and list mountpoints.
  4. Read nixos-anywhere --help and note --generate-hardware-config. That flag is how initrd modules still get generated when Disko owns mounts.