/architecture
Two repositories, one pinned release.
An opinionated NixOS for one person on one machine. One disk layout, one user, four desktops, two profiles. This page shows how the pieces fit, from the installer's first screen to the modules a machine is built from.
01The pieces
Installing and configuring are separate jobs.
installer
ephemeral-nixos
The ISO, the installer's screens, install.sh and the install test. It has no NixOS modules of its own. Its flake.nix is the machine flake that gets installed.
config
ephemeral-nixos-config
Every NixOS and Home Manager module a machine gets, the partition layout, the helper commands and their tests. The only place modules live.
machine
/persist/etc/nixos
flake.nix pinning a config release, plus settings.json, hardware-configuration.nix, apps.json and your own modules/.
The split means the installer can't drift from the system it installs: the partition layout, the modules and the locked nixpkgs all come from the one config release it pins. An installed machine never needs the installer again. It follows config releases on its own, through ephemeral-update. A change to the config reaches new installs only once the installer pins a release that has it, and the installer only moves to a release after its install test passes with it.
02Install pipeline
From the ISO to a running desktop.
Each step links to the file that implements it.
Boot the Ephemeral Nix ISO
Or the NixOS ISO with a verified clone. UEFI, Secure Boot off.
Installer screens
A Go program (Bubble Tea) asks one question per screen, checks each answer with the same rules as the scripts, and ends on a review.
install.sh
Does all the disk work, in stages. Nothing outside /tmp is written until the disk’s name has been typed.
- prepare
- confirm
- partition
- hardware
- install
The machine’s flake
flake.nix pinning a config release, settings.json with your answers, hardware-configuration.nix and apps.json. It is built in a temporary folder and evaluated before the disk is touched, then written to /persist/etc/nixos, with a read-only copy in /persist/etc/nixos-install.
Disk layout (disko)
The partition layout comes from the pinned config release. GPT, a 1G ESP, then LUKS2 with argon2id holding btrfs subvolumes.
lib.mkFlake
Merges the machine’s inputs over the config’s, and loads every module in the config’s modules/ and the machine’s own modules/ with import-tree.
Settings, then aspects
settings.json is checked against its schema on its own first. host-builder turns the typed settings into a list of aspects for nixosSystem.
NixOS and Home Manager desktop
nixos-install builds it and installs systemd-boot. From then on, every boot resets / and every ephemeral-update rebuilds from the machine’s flake.
ephemeral-update repeats steps 06 to 08 from /persist/etc/nixos.03Disk
One layout, encrypted below the filesystem.
/boot
kernel, initrd, systemd-boot. Mounted umask=0077. Not encrypted.
- root/deleted and recreated at every bootreset
- root-blanknot mountedread-only, empty snapshot of roottemplate
- nix/nixthe store; rebuilt by Nix, never resetkept
- persist/persist/home and every declared pathkept
- log/var/logjournal and logs, across bootskept
- swap/.swapvolswap file, inside the encryptionkept
modules/_disko-layout.nix. The disk test formats a VM disk with it and checks it. Changing any part of it is a breaking change.Everything but the ESP is inside LUKS2, the swap file included, so nothing on the system subvolumes is readable without the passphrase. Hibernation is off, because protecting the kernel image rules it out; suspend works. TRIM is passed through the encryption, which shows which blocks are in use but not what's in them.
04Releases
A machine only ever builds a released tag.
The installer's flake.nix pins a config tag, and its flake.lock pins the rest, so the same installer release always installs the same system. Installer v3.7.6 installs configv4.0.0.
On the machine, ephemeral-update checks for a newer config release before it rebuilds. It only offers a newer, well-formed tag, shows the changelog since yours, names a new major version before asking, and on yes changes the tag and updates the lock. Offline, it rebuilds what's pinned.
Releases are immutable. The config gets a patch release every Monday with the newest nixpkgs once the boot tests pass; other inputs, such as Home Manager and Stylix, come in a pull request a person reads first, because their code runs as root on every machine. Latest: config v4.1.0, installer v3.7.6.
{
description = "An Ephemeral Nix machine";
inputs = {
ephemeral.url = "github:the-wittch/ephemeral-nixos-config/v4.0.0";
# the installer runs nixpkgs tools by name
nixpkgs.follows = "ephemeral/nixpkgs";
};
outputs =
inputs:
inputs.ephemeral.lib.mkFlake {
inherit inputs;
root = ./.;
};
}05Dendritic modules
One feature, one file. No imports between them.
The config follows the dendritic pattern. Every .nix file under modules/ is a flake-parts module, loaded by import-tree; paths containing /_ are skipped, and that's the only exclusion. There is no import list to keep up to date.
A feature's NixOS config, Home Manager config, persistence, permissions and sandbox live in its own file. Files don't import each other by path. They add to shared modules, called aspects: flake.modules.nixos.<name> and flake.modules.homeManager.<name>.
An aspect that needs another imports it through config.ephemeral.aspects, which wraps each one with a key, so importing it from several places applies it once. The aspect names are a public interface: your machine's modules add to them, and renaming one is a breaking change.
modules/
├── boot.nix # systemd-boot, plymouth, tmpfs /tmp
├── ephemeral-root.nix # the rollback unit and root-reset-check
├── persist.nix # impermanence and the base kept paths
├── hardening.nix # kernel parameters and sysctls
├── opensnitch.nix # one feature: its config and its kept paths
├── host-builder.nix # the only place a machine is composed
├── aspects.nix # shared modules, each with a key
├── settings/_schema.nix # every setting and its default
├── desktop/ # gnome, plasma, niri, umbriel, noctalia
├── dotfiles/ # Home Manager: zsh, git, starship, neovim…
└── apps/ # the ephemeral-apps catalog, by category06Profiles and settings
Typed settings in, a list of aspects out.
Settings are evaluated once, on their own, before anything is composed. Their options and defaults are in settings/_schema.nix and nowhere else; a wrong value, such as "desktop": "kde", is reported with the allowed values. Then host-builder.nix, the only place a machine is composed, picks the aspects:
settings/_schema.nixhost-builder.nix| When | Aspects added | What they bring |
|---|---|---|
| always | base | boot, disk layout, root reset, persistence, users, hardening, networking, Nix settings |
| always | pc | imports base; adds audio, fonts, printing, Flatpak with Flathub, Podman, the wallpaper |
| profile = full | full<desktop> | Home Manager, Stylix, snapshots, Syncthing, SMB, scanning, disk health, nix-ld, direnv |
| profile = minimal | minimalminimal-<desktop> | a plain desktop, no Home Manager; finish it later with ephemeral-setup |
| settings | nvidialaptopfingerprinttailscale | each when its setting is on |
| useOpenSnitch, full only | opensnitch | on by default; outbound connections need your approval |
| settings, full only | extrascontroldpassSshAgent | ignored on the small install, and the rebuild warns that they are |
| apps.json, full only | <app>.nixos | each app picked in ephemeral-apps |
| your modules/ | … | every file in /persist/etc/nixos/modules, adding to any aspect |
nixpkgs.lib.nixosSystem → nixosConfigurations.nixosfull is the default. minimal is the installer's small install: a plain desktop to start from, turned into the full system later with ephemeral-setup. A full and a minimal variant of the same desktop have to agree on the login manager, keyring, wallet and persisted folders; desktop-parity checks it. Every setting is on settings.json.
07Declaring persistence
What a feature keeps is in the feature's file.
Everything that has to survive a reboot is listed in environment.persistence."/persist", from the impermanence module, by the feature that needs it. OpenSnitch keeps its rules; Tailscale its state; GNOME its login manager's.
The full list is collected at build time into /etc/ephemeral/kept, which ephemeral-doctor kept prints. The boot test reads the same list and checks each entry survives a reboot, so a path that's declared but not actually kept fails CI.
{
flake.modules.nixos.opensnitch =
{ lib, pkgs, ... }:
{
environment.persistence."/persist".directories = [ "/var/lib/opensnitch" ];
environment.systemPackages = [ pkgs.opensnitch-ui ];
services.opensnitch = {
enable = true;
settings = {
DefaultAction = "deny";
InterceptUnknown = true;
};
};
};
}08The rules
Written down, and checked against.
Both repositories keep an ARCHITECTURE.md listing the rules every change is held to: modules, the ephemeral root, disk and encryption, security, helper commands, releases, tests and docs. A change that breaks one needs a reason, written in the pull request and in that file.