Disko
20 min read
NixOS is a Linux distribution where everything is described as code, with one exception: during installation, the disk partitioning and formatting are manual steps. disko aims to correct this.

You write a declarative device tree — disks, partition tables, RAID sets, volume groups, pools and filesystems — and disko turns it into three shell scripts: destroy, format and mount. The same declaration is also fed back into NixOS as fileSystems, swapDevices and boot.initrd.luks.devices, so you never hand-write a hardware-configuration.nix filesystem section again.

Mental Model
A disko config is a tree of nested content. Every node has a type, and most nodes have a content attribute holding the next node down. The chain always ends in something mountable.

disk (/dev/nvme0n1)
└── content: gpt
├── partition ESP → content: filesystem (vfat, /boot)
└── partition luks → content: luks
└── content: lvm_pv (vg = "pool")

lvm_vg "pool"
├── lv root → content: filesystem (ext4, /)
└── lv home → content: filesystem (ext4, /home)

Two kinds of nodes exist:

• Entry points — the top-level attrsets under disko.devices. These are where disko starts walking.
• Content types — everything reachable via a content attribute.

Nodes that span multiple disks (lvm_vg, zpool, mdadm, bcachefs_filesystems) are declared as entry points and referenced by name from the partitions that feed them. That is why lvm_pv only carries vg = "pool" — the actual volume group lives at the top level.

Entry Points

AttributeContainsNotes
disko.devices.disk.<name>A physical block deviceThe usual starting point
disko.devices.lvm_vg.<name>LVM volume groupFed by lvm_pv content
disko.devices.mdadm.<name>Linux software RAID arrayFed by mdraid content
disko.devices.zpool.<name>ZFS poolFed by zfs content
disko.devices.bcachefs_filesystems.<name>bcachefs multi-device filesystemFed by bcachefs content
disko.devices.nodev.<mountpoint>Virtual filesystems (tmpfs, overlay, …)No backing device

Content Types

TypeValid asPurpose
gptdisk / mdadm contentGPT partition table (recommended)
tabledisk contentLegacy parted table, supports msdos. Deprecated — see table-to-gpt.md upstream
filesystempartition / lv / zvol contentPlain mkfs.<format>
swappartition / lv / zvol contentmkswap, optionally random-encrypted
lukspartition contentLUKS2 container, itself holding more content
lvm_pvpartition contentPhysical volume, joins a named lvm_vg
mdraidpartition contentMember of a named mdadm array
zfspartition contentvdev member of a named zpool
btrfspartition contentBtrfs filesystem with subvolumes
bcachefspartition contentMember of a named bcachefs filesystem

Note that gpt and table are only valid as device content (disk, mdadm), not inside a partition — a partition can't hold another partition table.

Installation

nix
{
inputs.disko.url = "github:nix-community/disko/latest";
inputs.disko.inputs.nixpkgs.follows = "nixpkgs";

outputs = { self, nixpkgs, disko }: {
nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
disko.nixosModules.disko
./disk-config.nix
];
};
};
}

The /latest in the URL pins to the latest tagged release rather than master.

Without flakes

nix
# npins / niv
{ imports = [ (sources.disko + "/module.nix") ]; }

# fetchTarball (pin the commit + hash in anything you care about)
{ imports = [ "${builtins.fetchTarball "https://github.com/nix-community/disko/archive/master.tar.gz"}/module.nix" ]; }

Usage
Standalone CLI
disko runs perfectly well without importing the module — handy from a rescue ISO:

shell
sudo nix --experimental-features "nix-command flakes" \
run github:nix-community/disko/latest -- \
--mode destroy,format,mount /tmp/disk-config.nix

Or against a flake, resolving .diskoConfigurations.<attr> first and falling back to the disko module of .nixosConfigurations.<attr>:

shell
sudo disko --mode destroy,format,mount --flake 'github:you/nixos-config#myhost'

Modes

ModeEffect
destroyUnmount filesystems and wipe the partition tables of the selected disks
formatCreate partition tables, pools, VGs, arrays and filesystems that don't exist yet
mountMount the tree under the root mountpoint
format,mountThe two above, in sequence
destroy,format,mountAll three. Formerly --mode disko

format is idempotent — it skips anything blkid already recognises. That makes format,mount safe to re-run when you have added a disk, and mount alone the right way to bring an existing installation back up for repair.

Flags

FlagMeaning
-m, --mode <mode>One of the modes above
-f, --flake <uri>Resolve the config from a flake
--arg / --argstr <name> <value>Pass arguments to nix-build, e.g. to parameterise disk names
--root-mountpoint <path>Where to mount the tree (default /mnt)
--dry-runPrint the path to the generated script instead of running it
--no-depsDon't pull another dependency closure into an in-memory installer
--debugset -x
--yes-wipe-all-disksSkip the interactive destruction confirmation — for automation

As a NixOS module
Importing disko.nixosModules.disko and setting disko.devices does two things at once: it generates the scripts, and it configures NixOS from the same tree.

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

Build outputs made available on the system:

config.system.build.<attr>What it is
diskoScriptdestroy + format + mount, with its dependency closure
formatScriptformat only
mountScriptmount only
destroyScriptdestroy only
*ScriptNoDepsSame, without the closure — for tiny installers
diskoImagesScriptBuilds .raw images in a VM, outside the store
diskoImagesSame, but inside the nix sandbox
vmWithDiskoInteractive VM using the real disk layout
installTestNixOS VM test that installs and reboots into the layout

Full install walkthrough

shell
# 1. identify the disk — prefer a stable /dev/disk/by-id path over sdX
lsblk
ls -l /dev/disk/by-id/

# 2. write /tmp/disk-config.nix (see templates below), set `device`

# 3. partition, format, mount to /mnt
sudo nix --experimental-features "nix-command flakes" \
run github:nix-community/disko/latest -- \
--mode destroy,format,mount /tmp/disk-config.nix

mount | grep /mnt # sanity check

# 4. generate the rest of the config WITHOUT filesystem stanzas
nixos-generate-config --no-filesystems --root /mnt
mv /tmp/disk-config.nix /mnt/etc/nixos/

# 5. import ./disk-config.nix + the disko module in configuration.nix, then
nixos-install
reboot

The --no-filesystems flag is the important part: disko owns fileSystems, and letting nixos-generate-config also write them produces conflicting definitions. Consider nixos-facter instead of nixos-generate-config for hardware detection.

For remote or unattended installs, nixos-anywhere drives disko over SSH for you — it kexecs into an installer, runs the disko script, and installs, all from one command.

Configuration Reference
disk

OptionDefaultDescription
device—Absolute path to the block device. Use /dev/disk/by-id/…
type—Must be "disk"
contentnullgpt, table, or a bare filesystem/luks/lvm_pv/…
destroytrueSet false to protect a disk from the destroy stage while still formatting/mounting others
imageNameattr nameOutput name when building disk images
imageSize"2G"Image size passed to qemu-img create

gpt

OptionDefaultDescription
partitions{}Attrset of partitions, keyed by name
efiGptPartitionFirsttruePlace the protective 0xEE entry first in the MBR — GRUB wants this

Per partition:

OptionDefaultDescription
type8300, or 8200 for swapsgdisk short code (sgdisk -L) or a full type GUID. EF00 = ESP, EF02 = BIOS boot
size"0"sgdisk size, e.g. 512M, 20G, or 100% for the rest of the disk
start / end"0" / +sizeExplicit placement; end = "-1G" leaves a gap at the disk end
priority1000 / 9001 / 100Creation order, lower first. 100% partitions get 9001 (last), EF02 gets 100 (first)
label<parent-type>-<parent>-<part>GPT partition name, max 36 chars — longer defaults get hashed
uuidnullFix the partition GUID (uuidgen -r). Distinct from the filesystem UUID
attributes[]GPT attribute bits: 0 required, 2 legacy-BIOS-bootable, 60 read-only, 62 hidden, 63 no-automount
alignment0, or 1 with sector unitsAlignment in sectors
hybridnull{ mbrPartitionType; mbrBootableFlag; } — adds an entry to a hybrid MBR
contentnullWhatever lives inside

A partition with no content is simply created and left alone, which is exactly what an EF02 BIOS-boot partition wants.

filesystem

OptionDefaultDescription
format—ext4, vfat, xfs, f2fs, btrfs, … Passed to mkfs.<format>
mountpointnullWhere to mount. null formats but never mounts
mountOptions[ "defaults" ]Written into fileSystems.<mp>.options
extraArgs[]Extra mkfs arguments, e.g. [ "-L" "root" ]

swap

OptionDefaultDescription
randomEncryptionfalseRe-key the swap with a random key at every boot. Rules out hibernation
resumeDevicefalseSet boot.resumeDevice — needed for hibernation
prioritynull0–32767, higher wins
discardPolicynullonce, pages or both — see swapon(8)
mountOptions[ "defaults" ]e.g. [ "nofail" ]

Set DISKO_SKIP_SWAP=1 in the environment to make the mount stage skip swapon — useful in low-memory or nested-VM situations.

luks

OptionDefaultDescription
name—Name of the /dev/mapper device
passwordFilenullFile read only during formatting. Not baked into the system
settings{}Passed straight through to boot.initrd.luks.devices.<name> — keyFile, keyFileSize, keyFileOffset, allowDiscards, fallbackToPassword, …
askPasswordtrue if no key/password givenPrompt interactively while formatting
additionalKeyFiles[]Extra keyslots to enroll
initrdUnlocktrueEmit the boot.initrd.luks.devices entry
extraFormatArgs[]e.g. [ "--pbkdf argon2id" ] for cryptsetup luksFormat
extraOpenArgs[]e.g. [ "--timeout 10" ] for cryptsetup luksOpen
enrollFido2falseEnroll a FIDO2 token via systemd-cryptenroll
extraFido2EnrollArgs[]Extra systemd-cryptenroll arguments
enrollRecovery= enrollFido2Also enroll a high-entropy recovery passphrase and print it as a QR code

passwordFile is a formatting-time secret. It must exist on the installer, not on the installed system — which is why nixos-anywhere has --disk-encryption-keys and the image builder has --pre-format-files.

lvm_pv / lvm_vg
lvm_pv has exactly one meaningful option, vg, naming the volume group it belongs to. The group itself:

lvm_vg.<name>.lvs.<lv>DefaultDescription
size—LVM size string. 100%FREE and friends work
lvm_typenullmirror, raid0, raid1, raid4, raid5, raid6, thin-pool, thinlv
poolnullFor thinlv: the thin-pool LV it lives in
extraArgs[]Extra lvcreate arguments
contentnullFilesystem, swap, …

Thin pools are created before regular LVs (priority 501 vs 1000), and anything sized with 100% is pushed to the back.

mdraid / mdadm
mdraid carries only name, pointing at a top-level mdadm array:

mdadm.<name>DefaultDescription
level1RAID level
metadata"default"Superblock version — 1.0 places it at the end, which some bootloaders need
extraArgs[]Extra mdadm --create arguments
contentnullUsually a gpt table, since an array is a device, not a partition

btrfs

OptionDefaultDescription
subvolumes{}Keyed by subvolume name, e.g. "/rootfs"
subvolumes.<n>.mountpointnullRequired unless a parent subvolume is already mounted
subvolumes.<n>.mountOptions[ "defaults" ]e.g. [ "compress=zstd" "noatime" ]
subvolumes.<n>.swap{}Swapfiles inside the subvolume: swap.myswap.size = "8G"
mountpointnullMount the top-level volume itself
swap{}Swapfiles on the top-level volume
extraArgs[][ "-f" ] forces over an existing signature

zfs / zpool
zfs content carries only pool. The pool:

zpool.<name>DefaultDescription
mode"" (stripe)mirror, raidz1…3, or a topology attrset with vdev / special / cache / log / spare / dedup groups
options{}Pool properties, e.g. ashift = "12", cachefile = "none"
rootFsOptions{}Root dataset properties: compression, acltype, xattr, encryption, …
mountpointnullMount the pool root
datasets{}Nested datasets

Datasets are either zfs_fs (options, mountOptions, mountpoint) or zfs_volume (adds size, extraArgs and a content chain, so you can put ext4 or swap on a zvol). Note the difference between the ZFS property options.mountpoint and disko's own mountpoint: the former is what ZFS records, the latter is what disko mounts during install. options.mountpoint = "legacy" plus a disko mountpoint gives you fileSystems-managed ZFS.

bcachefs
Multi-device bcachefs is split like LVM: each partition gets type = "bcachefs" with filesystem (which named filesystem it joins), an optional label for device groups (group_a.vdb2), and extraFormatArgs. The filesystem itself lives under bcachefs_filesystems.<name> with subvolumes, mountpoint, passwordFile, uuid and extraFormatArgs.

nodev
For filesystems with no backing device. The attribute name is the mountpoint:

nix
disko.devices.nodev."/tmp" = {
fsType = "tmpfs";
mountOptions = [ "size=200M" "mode=1777" ];
};

Hooks
Every node — entry point or content — accepts six escape hatches, each a block of shell:

preCreateHook, postCreateHook, preMountHook, postMountHook, preUnmountHook, postUnmountHook

The classic use is taking the blank snapshot an impermanence setup rolls back to:

nix
disko.devices.zpool.zroot.postCreateHook = ''
zfs snapshot zroot/root@blank
'';

Module Options

OptionDefaultDescription
disko.devices{}The device tree
disko.rootMountPoint"/mnt"Where the mount script mounts the tree
disko.enableConfigtrueWhether to also generate fileSystems / boot / swapDevices. Set false on installer images that merely carry the config
disko.checkScriptsfalseRun shellcheck over the generated scripts
disko.memSize1024Memory (MiB) for the image-building VM
disko.imageBuilder.*—Image generation: imageFormat (raw/qcow2), enableBinfmt, pkgs, kernelPackages, qemu, extraRootModules, extraPostVM, extraDependencies, copyNixStore, copyNixStoreThreads
disko.tests.*—bootCommands, efi, enableOCR, extraChecks, extraConfig for system.build.installTest

Disk Templates
Upstream ships two sets: disko-templates holds curated, best-practice layouts (single-disk-ext4, single-ext4-luks-and-double-zfs-mirror, zfs-impermanence), while the example directory holds ~40 configs that double as the regression test suite — broader coverage, but with test-only quirks (10M partitions, /tmp/secret.key) you must edit out.

shell
nix flake init --template github:nix-community/disko-templates#single-disk-ext4

The layouts below are trimmed for real use. In all of them, replace device with a stable /dev/disk/by-id/… path.

Minimal UEFI + ext4
The safe default. Boots on any UEFI machine.

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

umask=0077 on the ESP keeps the vfat mount from being world-readable, which otherwise trips systemd's /boot permission warnings.

BIOS + UEFI hybrid
Same disk boots on old and new firmware. Requires GRUB with efiSupport; disko sets boot.loader.grub.device for you.

nix
{
disko.devices.disk.main = {
type = "disk";
device = "/dev/disk/by-id/CHANGE-ME";
content = {
type = "gpt";
partitions = {
boot = {
size = "1M";
type = "EF02"; # BIOS boot partition, holds GRUB core.img
};
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
}

nix
boot.loader.grub = {
enable = true;
efiSupport = true;
efiInstallAsRemovable = true;
};

Btrfs with subvolumes
A layout that leaves room for snapshots and keeps /nix out of them.

nix
{
disko.devices.disk.main = {
type = "disk";
device = "/dev/disk/by-id/CHANGE-ME";
content = {
type = "gpt";
partitions = {
ESP = {
type = "EF00";
size = "1G";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
root = {
size = "100%";
content = {
type = "btrfs";
extraArgs = [ "-f" ];
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [ "compress=zstd" "noatime" ];
};
"/home" = {
mountpoint = "/home";
mountOptions = [ "compress=zstd" "noatime" ];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [ "compress=zstd" "noatime" ];
};
"/persist".mountpoint = "/persist";
"/log" = {
mountpoint = "/var/log";
mountOptions = [ "compress=zstd" "noatime" ];
};
"/swap" = {
mountpoint = "/.swapvol";
swap.swapfile.size = "8G";
};
};
};
};
};
};
};
}

Subvolume names and mountpoints are independent. A nested subvolume whose parent is already mounted needs no mountpoint of its own.

LUKS + LVM
Full-disk encryption with flexible volumes underneath. Everything but /boot is encrypted.

nix
{
disko.devices = {
disk.main = {
type = "disk";
device = "/dev/disk/by-id/CHANGE-ME";
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; # SSD TRIM; leaks used-block info
# passwordFile = "/tmp/secret.key"; # formatting only, no trailing newline
content = {
type = "lvm_pv";
vg = "pool";
};
};
};
};
};
};

lvm_vg.pool = {
type = "lvm_vg";
lvs = {
swap = {
size = "16G";
content = {
type = "swap";
resumeDevice = true;
};
};
root = {
size = "100%FREE";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
}

Fixed-size volumes must come before the 100%FREE one — disko already handles this via priority, but it's worth knowing why the ordering is not the attribute order.

LUKS + Btrfs, impermanence style
Root is wiped on every boot; only /nix and /persist survive.

nix
{
disko.devices.disk.main = {
type = "disk";
device = "/dev/disk/by-id/CHANGE-ME";
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";
content = {
type = "btrfs";
extraArgs = [ "-f" ];
postCreateHook = ''
MNT=$(mktemp -d)
mount -t btrfs /dev/mapper/crypted "$MNT"
trap 'umount "$MNT"; rm -rf "$MNT"' EXIT
btrfs subvolume snapshot -r "$MNT/root" "$MNT/root-blank"
'';
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [ "compress=zstd" "noatime" ];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [ "compress=zstd" "noatime" ];
};
"/persist" = {
mountpoint = "/persist";
mountOptions = [ "compress=zstd" "noatime" ];
};
};
};
};
};
};
};
};
}

The rollback itself is a boot.initrd.postDeviceCommands (or a systemd initrd service) that restores root-blank over root before the root filesystem is mounted. disko's job stops at making the pristine snapshot exist.

ZFS mirror with native encryption

nix
{
disko.devices = {
disk = {
a = {
type = "disk";
device = "/dev/disk/by-id/CHANGE-ME-A";
content = {
type = "gpt";
partitions = {
ESP = {
size = "1G";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" "nofail" ];
};
};
zfs = {
size = "100%";
content = {
type = "zfs";
pool = "zroot";
};
};
};
};
};
b = {
type = "disk";
device = "/dev/disk/by-id/CHANGE-ME-B";
content = {
type = "gpt";
partitions.zfs = {
size = "100%";
content = {
type = "zfs";
pool = "zroot";
};
};
};
};
};

zpool.zroot = {
type = "zpool";
mode = "mirror";
options.ashift = "12";
rootFsOptions = {
mountpoint = "none";
compression = "zstd";
acltype = "posixacl";
xattr = "sa";
"com.sun:auto-snapshot" = "false";
};
datasets = {
"enc" = {
type = "zfs_fs";
options = {
mountpoint = "none";
encryption = "aes-256-gcm";
keyformat = "passphrase";
keylocation = "prompt";
};
};
"enc/root" = {
type = "zfs_fs";
mountpoint = "/";
options."com.sun:auto-snapshot" = "true";
};
"enc/nix" = {
type = "zfs_fs";
mountpoint = "/nix";
options.atime = "off";
};
"enc/home" = {
type = "zfs_fs";
mountpoint = "/home";
options."com.sun:auto-snapshot" = "true";
};
};
};
};
}

ZFS needs networking.hostId set to a unique 8-hex-digit value, and boot.supportedFilesystems = [ "zfs" ]. Encrypting a parent dataset with mountpoint = "none" means one passphrase unlocks every child.

For a more complex pool, mode takes a topology instead of a string:

nix
mode.topology = {
type = "topology";
vdev = [{ mode = "mirror"; members = [ "a" "b" ]; }];
special = [{ mode = "mirror"; members = [ "nvme1" "nvme2" ]; }];
cache = [ "l2arc" ];
};

Members refer to the disk names in disko.devices.disk, not device paths.

mdadm RAID1
The array is a device, so it gets its own partition table.

nix
{
disko.devices = {
disk = builtins.listToAttrs (map (d: {
name = "disk${toString d.i}";
value = {
type = "disk";
device = d.dev;
content = {
type = "gpt";
partitions = {
boot = { size = "1M"; type = "EF02"; };
raid = {
size = "100%";
content = { type = "mdraid"; name = "raid1"; };
};
};
};
};
}) [
{ i = 1; dev = "/dev/disk/by-id/CHANGE-ME-A"; }
{ i = 2; dev = "/dev/disk/by-id/CHANGE-ME-B"; }
]);

mdadm.raid1 = {
type = "mdadm";
level = 1;
content = {
type = "gpt";
partitions.primary = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
}

Because a disko config is ordinary Nix, generating the per-disk attrset with map/listToAttrs scales to any number of members. The example/with-lib.nix upstream shows the same trick.

tmpfs /tmp and swap partitions

nix
{
disko.devices = {
disk.main = {
type = "disk";
device = "/dev/disk/by-id/CHANGE-ME";
content = {
type = "gpt";
partitions = {
ESP = {
size = "1G";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
root = {
end = "-16G"; # leave the tail for swap
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
swap = {
size = "100%";
content = {
type = "swap";
resumeDevice = true;
discardPolicy = "both";
};
};
};
};
};

nodev."/tmp" = {
fsType = "tmpfs";
mountOptions = [ "size=8G" "mode=1777" ];
};
};
}

Swap randomEncryption = true gives you an encrypted scratch swap at the cost of hibernation; the two options are mutually exclusive in practice.

Beyond Installation
disko-install
nixos-install plus partitioning in one step, and it can run from your workstation rather than the target machine — which makes it the neat way to build a bootable USB stick carrying a full NixOS configuration.

shell
sudo nix run 'github:nix-community/disko/latest#disko-install' -- \
--flake '/etc/nixos#mymachine' --disk main /dev/sda

--disk <name> <device> overrides the device written in the config, so the same flake can target whatever is plugged in. Options: --mode format|mount (mount only, to repair an existing install without touching data), --write-efi-boot-entries (off by default, since the target may be portable), --extra-files SRC DEST, --system-config JSON, --mount-point PATH, --dry-run.

It also works fully offline if the installer's closure includes diskoScript, its drvPath and the flake inputs — see the upstream disko-install.md.

Disk images
Build a .raw from a NixOS configuration, without ever touching real hardware:

shell
nix build .#nixosConfigurations.myhost.config.system.build.diskoImagesScript
sudo ./result --build-memory 2048
ls -la main.raw

The script boots a VM, runs disko inside it, installs, and drops the images in the working directory named after disko.devices.disk.<name> (override with imageName, size with imageSize). Two flags handle secrets:

• --pre-format-files <src> <dst> — placed before disko runs. This is how LUKS key files get in.
• --post-format-files <src> <dst> — placed on the finished image, owned by root:root.

system.build.diskoImages does the same inside the nix sandbox: reproducible and cacheable, but slower and with no secure way to inject secrets.

Cross-architecture images work through binfmt:

nix
disko.imageBuilder = {
enableBinfmt = true;
pkgs = inputs.nixpkgs.legacyPackages.x86_64-linux;
kernelPackages = inputs.nixpkgs.legacyPackages.x86_64-linux.linuxPackages_latest;
};
nixpkgs.hostPlatform = "aarch64-linux";

There is no auto-resize yet, so imageSize has to be generous enough for the whole closure.

Interactive VM
Boot your real disk layout in QEMU before committing it to hardware:

shell
nix run -L '.#nixosConfigurations.myhost.config.system.build.vmWithDisko'

Unlike the stock config.system.build.vm, this one actually runs the disko scripts, so LUKS prompts, ZFS pools and subvolume mounts all behave as they will on the real machine. Configure the variant through virtualisation.vmVariantWithDisko; disk sizes come from imageSize, memory from disko.memSize.

Testing
system.build.installTest is a NixOS VM test that installs the configuration onto virtual disks and boots it. disko.tests.bootCommands lets you type an encryption passphrase, disko.tests.extraChecks asserts on the booted system:

nix
disko.tests = {
extraChecks = ''
machine.succeed("test -e /persist/etc/machine-id")
machine.succeed("btrfs subvolume list / | grep -q root-blank")
'';
};

nixos-anywhere exposes the same idea as --vm-test, which is the cheapest way to find out that your layout is wrong before you wipe a remote server.

Partial and repeated runs
Because the three stages are separate scripts, a few non-obvious workflows fall out:

• Adding a disk — extend the config, run --mode format,mount. Existing filesystems are detected and skipped.
• Rescuing a system — boot an ISO, run --mode mount --flake '…#myhost', get the whole tree (including LUKS and ZFS) mounted at /mnt, nixos-enter.
• Protecting data disks — set destroy = false on a disk to keep the destroy stage away from it while the root disk is reinstalled.
• Parameterised layouts — --argstr disk /dev/sdb against a config that takes arguments lets one file serve many machines.

Gotchas
• Dual boot is not supported. disko owns the whole disk; there is no way to describe "keep partitions 1–3".
• Use /dev/disk/by-id/. sda and nvme0n1 are enumeration-order-dependent and will eventually point at the wrong disk.
• nixos-generate-config --no-filesystems. Otherwise you get two competing fileSystems definitions.
• enableConfig = false on installers. An installer image that merely carries a disko config must not try to mount that config as its own root.
• The examples are tests. Partitions of 10M, keys at /tmp/secret.key and extraArgs = [ "-f" ] exist to exercise disko, not to be copied verbatim.
• format is idempotent, destroy is not. Re-running destroy,format,mount on a live system does exactly what it says.
• Password files must have no trailing newline if you also want to use them for interactive login: echo -n "password" > /tmp/secret.key.
• ZFS wants networking.hostId, and the pool import will fail at boot without it.

• nixos-anywhere — remote unattended installs driven by disko
• nixos-facter — hardware detection to pair with disko's disk layer
• nixos-images — installer images to run disko from
• lanzaboote — Secure Boot, which pairs naturally with LUKS layouts
• sops-nix — secrets for the installed system, once disko has made a place to put them
• Filesystems: Btrfs · ZFS · LVM · LUKS · RAID · bcachefs · Ext4 · XFS · tmpFS