Declare the disk layout with disko
Installing used to mean six manual steps through Calamares, one of which
copied hardware-configuration.nix into the repo — a step that fails
silently unless the file is also git added, since Nix only sees tracked
files. It also did not scale past one machine.
modules/disk.nix defines moonarch.disk.{enable,device,encrypt,espSize,
swapSize} and builds disko.devices from it, so another machine is a
device path rather than a new layout. Installing is a single
disko-install run from the ISO.
hardware-configuration.nix is no longer needed: nvme, ahci, sd_mod and
the USB HID modules are in the initrd via
boot.initrd.includeDefaultModules, and nixpkgs.hostPlatform comes from
nixosSystem. The testvm copy described a qcow2 that no longer exists and
is deleted; that host now uses the module against /dev/vda.
The user password no longer arrives with the installer, so it is set
once via nixos-enter after installing instead of committing a hash.
This commit is contained in:
1 parent
9988dc1e7b
commit
1ec12da9ff
7 files changed
+279
-81
No files matched your search
@@ -1,5 +1,46 @@
|
||||
# Decisions
|
||||
|
||||
## 2026-08-11 – Disk layout via disko after all, as a shared module
|
||||
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
- **Why**: The Calamares route decided below turned out to be six manual steps,
|
||||
one of which — copying `hardware-configuration.nix` into the repo — fails
|
||||
silently unless the file is also `git add`ed, because Nix only sees tracked
|
||||
files in a git repository. It also does not scale: there will be more
|
||||
machines than this one, and each would repeat the whole procedure.
|
||||
- **Tradeoffs**: The argument against disko was that no target machine existed
|
||||
and device paths would have to be guessed. That has expired. Keeping
|
||||
Calamares would avoid writing a layout module, but leaves partitioning,
|
||||
encryption and subvolumes outside version control, where they cannot be
|
||||
reviewed or repeated.
|
||||
- **How**: `modules/disk.nix` defines `moonarch.disk.{enable,device,encrypt,
|
||||
espSize,swapSize}` and builds `disko.devices` from it, so a new machine is a
|
||||
device path rather than a new layout. Installation is a single
|
||||
`disko-install --flake … --disk main <device>` from the ISO.
|
||||
`hardware-configuration.nix` is not needed: `nvme`, `ahci`, `sd_mod` and the
|
||||
USB HID modules are in the initrd by default via
|
||||
`boot.initrd.includeDefaultModules`, and `nixpkgs.hostPlatform` comes from
|
||||
`nixosSystem`. `hosts/testvm` uses the same module against `/dev/vda`; its
|
||||
generated file was deleted, having described the UUIDs of a qcow2 that no
|
||||
longer exists.
|
||||
- **Consequence**: The user password no longer arrives with the installer.
|
||||
`users.users.dkressler` declares none, so it is set once via `nixos-enter`
|
||||
after installing rather than committing a hash to the repository.
|
||||
|
||||
## 2026-08-11 – Swapfile without hibernation
|
||||
|
||||
- **Who**: ClaudeCode, on Dominik's request for a recommendation
|
||||
- **Why**: The layout module needs a swap default.
|
||||
- **Tradeoffs**: Hibernation needs swap at least the size of RAM plus a
|
||||
`resume_offset` kernel parameter. On btrfs that offset changes whenever the
|
||||
swapfile is recreated, which is a standing source of breakage. Avoiding it
|
||||
means LUKS → LVM → btrfs LV plus a swap LV, one layer more. A swap partition
|
||||
cannot be resized later without repartitioning; a swapfile can.
|
||||
- **How**: 8G swapfile in a `/.swapvol` subvolume, no hibernation, no LVM.
|
||||
Sized as a safety valve against memory pressure, not as a replacement for the
|
||||
30 GiB of RAM. Revisit by growing the swapfile and adding `resume_offset` —
|
||||
neither needs a reinstall.
|
||||
|
||||
## 2026-08-07 – Separate repo instead of Nix files in the application repos
|
||||
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
|
||||
@@ -24,6 +24,7 @@ pkgs/ Build recipes
|
||||
sweet-cursors.nix cursor theme
|
||||
moonarch-scripts.nix battery and camera helpers
|
||||
modules/
|
||||
disk.nix partitioning: GPT, optional LUKS2, btrfs subvolumes
|
||||
desktop.nix /etc deployment, package set, theme
|
||||
greetd.nix greetd + moongreet
|
||||
services.nix systemd system and user services
|
||||
@@ -57,58 +58,85 @@ sudo nixos-rebuild switch --flake .#testvm
|
||||
|
||||
## Installing on a machine
|
||||
|
||||
The disk layout is not declared here. The graphical installer handles
|
||||
partitioning, encryption and hardware detection, and produces the one file that
|
||||
cannot be written in advance: `hardware-configuration.nix` with the machine's
|
||||
UUIDs, LUKS devices and kernel modules.
|
||||
The disk layout is declared in `modules/disk.nix`, so nothing has to be
|
||||
generated on the target machine and no file travels back into this repo. Boot
|
||||
the NixOS ISO and run one command.
|
||||
|
||||
1. Boot the NixOS ISO and run the installer. Choose `dkressler` as the user
|
||||
name — that is what `hosts/thinkpad/default.nix` declares. Pick btrfs,
|
||||
snapper expects it.
|
||||
2. Reboot into the fresh system. It runs on the generated configuration,
|
||||
without this repo.
|
||||
3. Enable flakes and install git — neither is available yet:
|
||||
> **This erases the disk given to `--disk`.** Check the path against `lsblk` on
|
||||
> the machine you are standing in front of. Do not copy the path below without
|
||||
> looking — it is this ThinkPad's SSD.
|
||||
|
||||
1. Partition and install in one go:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/nixos/configuration.nix
|
||||
# nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
||||
# environment.systemPackages = with pkgs; [ git ];
|
||||
sudo nixos-rebuild switch
|
||||
sudo nix --experimental-features "nix-command flakes" run \
|
||||
'github:nix-community/disko/latest#disko-install' -- \
|
||||
--flake 'git+https://gitea.moonarch.de/nevaforget/moonix.git#thinkpad' \
|
||||
--disk main /dev/disk/by-id/nvme-SKHynix_HFS001TEJ9X102N_SDB7N7740101A8708
|
||||
```
|
||||
|
||||
4. Clone this repo and take over the hardware file:
|
||||
The LUKS passphrase is asked for twice while formatting. The repo is fetched
|
||||
from the flake reference, so the ISO needs no `git` and nothing is cloned.
|
||||
|
||||
2. Set the user password. Nothing has created one yet, and greetd is the only
|
||||
way in:
|
||||
|
||||
```bash
|
||||
git clone https://gitea.moonarch.de/nevaforget/moonix.git
|
||||
cp /etc/nixos/hardware-configuration.nix moonix/hosts/thinkpad/
|
||||
sudo nixos-enter --root /mnt -c 'passwd dkressler'
|
||||
```
|
||||
|
||||
5. Add the import to `hosts/thinkpad/default.nix`:
|
||||
3. Reboot.
|
||||
|
||||
To skip step 2, put a `mkpasswd -m sha-512` hash into
|
||||
`users.users.dkressler.initialHashedPassword` — the commented-out line in
|
||||
`hosts/thinkpad/default.nix` shows where. That commits a password hash to the
|
||||
repository.
|
||||
|
||||
`hardware-configuration.nix` is not used and not needed. NixOS puts `nvme`,
|
||||
`ahci`, `sd_mod` and the USB HID modules into the initrd by default
|
||||
(`boot.initrd.includeDefaultModules`), `nixpkgs.hostPlatform` comes from the
|
||||
flake, and the `nixos-hardware` profile for the T14 Gen 3 (AMD) covers
|
||||
firmware, microcode, power management and graphics quirks.
|
||||
|
||||
The generated `fileSystems` entries point at `/dev/disk/by-partlabel/`, so the
|
||||
booted system does not care whether the disk enumerates the same way next time.
|
||||
Only the partitioning step reads `moonarch.disk.device`.
|
||||
|
||||
### Adding another machine
|
||||
|
||||
1. New directory under `hosts/`, with a `default.nix` holding the machine
|
||||
specific settings — hostname, users, locale, bootloader.
|
||||
2. Point the shared layout at its disk:
|
||||
|
||||
```nix
|
||||
imports = [ ./hardware-configuration.nix ];
|
||||
moonarch.disk = {
|
||||
enable = true;
|
||||
device = "/dev/disk/by-id/..."; # check against lsblk
|
||||
};
|
||||
```
|
||||
|
||||
6. Switch and reboot:
|
||||
`encrypt = false` drops the LUKS layer, `swapSize = ""` the swapfile, and
|
||||
`espSize` resizes the ESP. Everything else is shared.
|
||||
3. Add a `nixosConfigurations.<name>` entry in `flake.nix`.
|
||||
4. Install with the same `disko-install` command, with `#<name>` and that
|
||||
machine's disk.
|
||||
|
||||
```bash
|
||||
sudo nixos-rebuild switch --flake ~/moonix#thinkpad
|
||||
```
|
||||
Without a matching `nixos-hardware` profile, set
|
||||
`hardware.cpu.intel.updateMicrocode` or `hardware.cpu.amd.updateMicrocode` in
|
||||
the host file — the profile is what provides it for the ThinkPad.
|
||||
|
||||
If step 6 breaks the system, pick the previous generation in the boot menu —
|
||||
the installer's configuration stays available as generation 1.
|
||||
### Disk layout
|
||||
|
||||
The `nixos-hardware` profile for the T14 Gen 3 (AMD) is already wired up:
|
||||
firmware, power management and graphics quirks come with it.
|
||||
| Partition / subvolume | Mountpoint | Notes |
|
||||
|---|---|---|
|
||||
| ESP, `espSize` (1G) | `/boot` | vfat, `umask=0077` |
|
||||
| `root` | `/` | btrfs, `compress=zstd`, `noatime` |
|
||||
| `home` | `/home` | same |
|
||||
| `nix` | `/nix` | same; separate to keep the store out of snapshots |
|
||||
| `swap` | `/.swapvol` | swapfile, `swapSize` (8G) |
|
||||
|
||||
Until step 5 is done, `nixosConfigurations.thinkpad` does not evaluate:
|
||||
|
||||
```
|
||||
error: Failed assertions:
|
||||
- The 'fileSystems' option does not specify your root file system.
|
||||
```
|
||||
|
||||
That is expected — the filesystems live in `hardware-configuration.nix`.
|
||||
With `encrypt = true` everything below the ESP sits in a LUKS2 container named
|
||||
`crypted`. Hibernation is not set up — see `DECISIONS.md`.
|
||||
|
||||
## Version bumps
|
||||
|
||||
@@ -143,6 +171,22 @@ overwrites later edits.
|
||||
`hosts/testvm` targets a QEMU guest. The launcher script lives outside this
|
||||
repo at `~/VMs/moonix-vm.sh`.
|
||||
|
||||
It uses the same layout module as real hardware, pointed at `/dev/vda` — the
|
||||
launcher attaches the qcow2 as `if=virtio` and there is no `by-id` path for it.
|
||||
LUKS stays enabled and the swapfile shrinks to 2G. Installing the guest is
|
||||
therefore also the test for `modules/disk.nix`.
|
||||
|
||||
Recreating the disk from scratch:
|
||||
|
||||
```bash
|
||||
qemu-img create -f qcow2 ~/VMs/moonix.qcow2 60G
|
||||
cp /usr/share/edk2/x64/OVMF_VARS.4m.fd ~/VMs/moonix-vars.fd
|
||||
~/VMs/moonix-vm.sh ~/Downloads/nixos-graphical-*.iso
|
||||
```
|
||||
|
||||
Then run the `disko-install` command from above inside the live session, with
|
||||
`#testvm` and `--disk main /dev/vda`.
|
||||
|
||||
It deliberately contains no testing shortcuts — no passwordless sudo, no SSH
|
||||
password authentication. Set those at runtime when needed, for example through
|
||||
a drop-in under `/run/systemd/system/`, so they do not end up in version
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
nixos-hardware.url = "github:NixOS/nixos-hardware";
|
||||
|
||||
# Declarative partitioning. Consumed by modules/disk.nix.
|
||||
disko = {
|
||||
url = "github:nix-community/disko/latest";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Project sources. These repos are not flakes themselves, they are
|
||||
# consumed as plain source trees pinned to a release tag.
|
||||
@@ -82,6 +87,8 @@
|
||||
# module without threading arguments through by hand.
|
||||
nixosModules.moonarch = { ... }: {
|
||||
imports = [
|
||||
inputs.disko.nixosModules.disko
|
||||
./modules/disk.nix
|
||||
./modules/desktop.nix
|
||||
./modules/greetd.nix
|
||||
./modules/services.nix
|
||||
|
||||
@@ -4,7 +4,15 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
imports = [ ./hardware-configuration.nix ];
|
||||
# The launcher attaches the qcow2 as if=virtio, so the guest sees /dev/vda.
|
||||
# There is no by-id path for it — the disk only exists inside the VM.
|
||||
# Encryption stays on: this is where the LUKS layer gets exercised before it
|
||||
# runs on real hardware.
|
||||
moonarch.disk = {
|
||||
enable = true;
|
||||
device = "/dev/vda";
|
||||
swapSize = "2G";
|
||||
};
|
||||
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# Do not modify this file! It was generated by ‘nixos-generate-config’
|
||||
# and may be overwritten by future invocations. Please make changes
|
||||
# to /etc/nixos/configuration.nix instead.
|
||||
{ config, lib, pkgs, modulesPath, ... }:
|
||||
|
||||
{
|
||||
imports =
|
||||
[ (modulesPath + "/profiles/qemu-guest.nix")
|
||||
];
|
||||
|
||||
boot.initrd.availableKernelModules = [ "ata_piix" "virtio_pci" "floppy" "sr_mod" "virtio_blk" ];
|
||||
boot.initrd.kernelModules = [ ];
|
||||
boot.kernelModules = [ "kvm-amd" ];
|
||||
boot.extraModulePackages = [ ];
|
||||
|
||||
fileSystems."/" =
|
||||
{ device = "/dev/disk/by-uuid/b88b75df-3999-4110-9e37-b8b76bf8206d";
|
||||
fsType = "btrfs";
|
||||
};
|
||||
|
||||
fileSystems."/home" =
|
||||
{ device = "/dev/disk/by-uuid/b88b75df-3999-4110-9e37-b8b76bf8206d";
|
||||
fsType = "btrfs";
|
||||
options = [ "subvol=home" ];
|
||||
};
|
||||
|
||||
fileSystems."/nix" =
|
||||
{ device = "/dev/disk/by-uuid/b88b75df-3999-4110-9e37-b8b76bf8206d";
|
||||
fsType = "btrfs";
|
||||
options = [ "subvol=nix" ];
|
||||
};
|
||||
|
||||
fileSystems."/boot" =
|
||||
{ device = "/dev/disk/by-uuid/77FC-6FE3";
|
||||
fsType = "vfat";
|
||||
options = [ "fmask=0077" "dmask=0077" ];
|
||||
};
|
||||
|
||||
swapDevices =
|
||||
[ { device = "/dev/disk/by-uuid/47747ffb-51b7-44c5-a2f3-6db564497c5c"; }
|
||||
];
|
||||
|
||||
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
# ABOUTME: Host configuration for the ThinkPad T14 Gen 3 (AMD).
|
||||
# ABOUTME: hardware-configuration.nix is added during installation, see README.
|
||||
# ABOUTME: Disk layout comes from modules/disk.nix, nothing is generated on the machine.
|
||||
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
# Partitioning, LUKS and the btrfs subvolumes. The device path is the only
|
||||
# machine specific value; the layout is shared with every other host.
|
||||
moonarch.disk = {
|
||||
enable = true;
|
||||
device = "/dev/disk/by-id/nvme-SKHynix_HFS001TEJ9X102N_SDB7N7740101A8708";
|
||||
};
|
||||
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
boot.kernelPackages = pkgs.linuxPackages_zen;
|
||||
@@ -33,6 +40,14 @@
|
||||
description = "Dominik Kressler";
|
||||
extraGroups = [ "networkmanager" "wheel" "video" "input" "plugdev" "docker" ];
|
||||
shell = pkgs.zsh;
|
||||
# No password is declared here, so a freshly installed system has no
|
||||
# /etc/shadow entry for this account and greetd cannot let anyone in. The
|
||||
# password is set once after installing, see README.
|
||||
#
|
||||
# To make it declarative instead, put the output of `mkpasswd -m sha-512`
|
||||
# in initialHashedPassword. That commits a hash to the repository, which is
|
||||
# offline attackable if the repository ever leaks:
|
||||
# initialHashedPassword = "$6$...";
|
||||
};
|
||||
|
||||
# Snapshots of root and home. snap-pac has no counterpart here: NixOS keeps
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# ABOUTME: Declarative disk layout for every host: GPT, optional LUKS2, btrfs.
|
||||
# ABOUTME: A host sets moonarch.disk.device; the layout itself is shared.
|
||||
|
||||
{ config, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.moonarch.disk;
|
||||
|
||||
mountOptions = [ "compress=zstd" "noatime" ];
|
||||
|
||||
subvolumes = {
|
||||
"/root" = { mountpoint = "/"; inherit mountOptions; };
|
||||
"/home" = { mountpoint = "/home"; inherit mountOptions; };
|
||||
# Separate so the store stays out of the snapper snapshots of /. It is
|
||||
# reproducible from the flake and would only inflate them.
|
||||
"/nix" = { mountpoint = "/nix"; inherit mountOptions; };
|
||||
} // lib.optionalAttrs (cfg.swapSize != "") {
|
||||
# No compression or noatime here: disko creates the swapfile inside this
|
||||
# subvolume, and btrfs requires it to be nodatacow, which it sets itself.
|
||||
"/swap" = {
|
||||
mountpoint = "/.swapvol";
|
||||
swap.swapfile.size = cfg.swapSize;
|
||||
};
|
||||
};
|
||||
|
||||
btrfs = {
|
||||
type = "btrfs";
|
||||
# Overwrite an existing signature instead of asking. The device is erased
|
||||
# by this point either way.
|
||||
extraArgs = [ "-f" ];
|
||||
inherit subvolumes;
|
||||
};
|
||||
|
||||
# Neither passwordFile nor settings.keyFile is set, which makes disko's
|
||||
# askPassword default to true: it prompts for the passphrase twice while
|
||||
# formatting and never puts it on a command line.
|
||||
luks = {
|
||||
type = "luks";
|
||||
name = "crypted";
|
||||
# Lets TRIM reach the SSD through the container. The tradeoff is that the
|
||||
# pattern of used blocks becomes visible on the raw device.
|
||||
settings.allowDiscards = true;
|
||||
content = btrfs;
|
||||
};
|
||||
in
|
||||
{
|
||||
options.moonarch.disk = {
|
||||
enable = lib.mkEnableOption "the declarative disk layout";
|
||||
|
||||
device = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "/dev/disk/by-id/nvme-eui.0123456789abcdef";
|
||||
description = ''
|
||||
Whole disk to partition, as a /dev/disk/by-id/ path — that one survives
|
||||
enumeration changes, unlike /dev/nvme0n1.
|
||||
|
||||
This value decides which disk gets erased. Check it against lsblk on the
|
||||
target machine before installing.
|
||||
|
||||
Only the partitioning reads it. The generated fileSystems entries go
|
||||
through /dev/disk/by-partlabel/, so a booted system does not depend on
|
||||
the path staying the same.
|
||||
'';
|
||||
};
|
||||
|
||||
encrypt = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Put the filesystem inside a LUKS2 container. The layout is otherwise
|
||||
identical, so a machine that does not need encryption — a throwaway VM —
|
||||
gets the same subvolumes.
|
||||
'';
|
||||
};
|
||||
|
||||
espSize = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "1G";
|
||||
description = ''
|
||||
Size of the EFI system partition. Every boot entry keeps its kernel and
|
||||
initrd here, so this scales with the number of generations kept.
|
||||
'';
|
||||
};
|
||||
|
||||
swapSize = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "8G";
|
||||
example = "";
|
||||
description = ''
|
||||
Size of the swapfile in the /.swapvol subvolume. Empty string creates no
|
||||
swap at all.
|
||||
|
||||
Sized as a safety valve, not as a replacement for RAM. Hibernation needs
|
||||
at least as much swap as RAM plus a resume_offset kernel parameter, which
|
||||
is not set up here — see DECISIONS.md.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
disko.devices.disk.main = {
|
||||
type = "disk";
|
||||
device = cfg.device;
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions = {
|
||||
ESP = {
|
||||
size = cfg.espSize;
|
||||
type = "EF00";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
mountOptions = [ "umask=0077" ];
|
||||
};
|
||||
};
|
||||
# Named the same whether or not it is encrypted, so the partlabel the
|
||||
# fileSystems entries point at does not depend on cfg.encrypt.
|
||||
root = {
|
||||
size = "100%";
|
||||
content = if cfg.encrypt then luks else btrfs;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user