Declarative Disk Partitioning with Disko
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.\"/\".deviceYou 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.42Output (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
fdiskrunbook. by-idon real disks./dev/vdaonly on single-disk lab VMs.- LUKS passphrase / key file is provisioning state, not a Nix string.
- Same Disko file for
nixos-anywhereand forfileSystemsin the running config. @nixand@persistas separate subvolumes before you turn on impermanence.
Try this
- On a disposable QEMU VM, apply Case 1.
findmnt / /bootafter Disko mount. Confirm/dev/vda1is vfat and/dev/vda2is ext4 (names may vary). ls -l /dev/disk/by-idon any Linux box and pick the id you would use. Do not run Disko there.- Eval
config.fileSystemsfrom a flake that imports Disko and list mountpoints. - Read
nixos-anywhere --helpand note--generate-hardware-config. That flag is how initrd modules still get generated when Disko owns mounts.