Nixie is a reusable NixOS platform with one installer ISO and two profiles: a hardened server that runs services as isolated Incus guests declared in Nix with a web control panel, and a desktop with a complete Hyprland environment. A profile is chosen at install; a system contains only its own profile's software. Your machines, guests and choices live in a small site repository; this repository is the platform and holds nothing specific to anyone.
Every picture and video below is a real run in a virtual machine: a NixOS
test that boots the machine, drives it and screenshots or records it.
nix run .#media regenerates the lot, and docs/media/SHOTLIST.md names the
run and the commit behind each file. The gallery stays under 100 MB with each
video under 8 MB, so it lives in git with no large-file storage; that command
fails rather than exceed it. GitHub does not play the videos inline, so they
are linked.
The wizard on the machine's own screen, and the same pages from a browser on another device. The steps are Machine, Disks, Name, Security, Network, Services and, on a desktop, Desktop.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Review shows a summary of every step, opens each file the install uses in an
editor with completion and help for every nixie.* option, and checks that
the machine evaluates before Install is allowed. Install is a checklist with a
progress bar per step.
![]() |
![]() |
After the restart, setup runs by itself: steps this machine does not use are skipped without being shown, and it stops only for the Secure Boot restart and for the disk passphrase and PIN.
The splash in the machine's finish, over a quiet boot from the loader on: the
mark and an indeterminate bar while it starts, the attestation code under
everything while anything is asked, and the TPM PIN and the passphrase on the
same screen, with a note when one was wrong and while the disk opens. Hardened
machines get it too, and the duress passphrase works at every prompt.
nixie.host.splashTheme swaps in any Plymouth theme: one Plymouth ships, a
package, or a directory from any repository.
![]() |
![]() |
Every screen in all three finishes; here Graphite and Paper. A walk-through
video: docs/media/panel-walkthrough.webm.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
The front panel on the first text console, the kiosk's lock page, Cockpit with the Nixie tokens, and the desktop profile's Hyprland session.
![]() |
![]() |
![]() |
![]() |
More of the desktop: the launcher, notification centre, control centre,
calendar, wallpaper picker, lock screen and power menu are in docs/media/,
with videos of the window motion
(desktop-motion.webm) and a runtime finish
switch (desktop-finish-runtime.webm).
- An x86_64 machine with UEFI firmware (CSM off). The ISO starts in BIOS mode too, but only to say so: installing needs UEFI (in VirtualBox, Settings, System, Enable EFI).
- A TPM 2.0 is optional; it enables TPM binding with a PIN and attestation.
- Any GPU or none. NVIDIA cards get the proprietary driver with modesetting
so the console keeps rendering; giving a card to a guest as an Incus
gpudevice shares it, VFIO passthrough to a VM takes it away from the host (docs/console.md). - Memory: 4 GB is enough for a server host; guests add their own.
nix build .#nixie-isomakesresult/iso/nixie_<version>_x86_64-linux.iso; write it to a USB stick (or attach it to a VM with EFI turned on) and boot it. The menu offers the graphical wizard on this screen, a browser on another device, or the terminal.- Use the wizard on the machine's screen, or open the printed
https://<address>:9443/on another device, compare the fingerprint and enter the pairing code. - Walk the steps: Machine, Disks, Name, Security, Network, Services, and Desktop on a desktop. Each field shows the first sentence of its help, with More for the rest; anything advanced is behind More options. A field the machine can answer for you offers a list rather than a blank: the time zone comes from this machine's own tzdata, and a backup folder can be picked from the drives it can see, which are mounted when picked. The Machine step also asks how much security you want: Standard leaves each feature a choice, Hardened turns them all on (TPM and PIN, an attestation code, Secure Boot, a duress passphrase, USB device blocking, memory encryption, key-only SSH, a second factor) and walks through every one of them, asking for what each needs.
- Review: a summary of every step, and every file the install uses
(
site.nix, the machine's ownconfiguration.nix,hardware.nix,guests.nix,data.nix,flake.nix) in an editor with Nix highlighting, completion and help for everynixie.*option. The machine is evaluated the way the install will build it and errors are marked on their line; Install waits for that check to pass. Then install and restart. Installing is a checklist with a progress bar per step. - Setup carries on by itself after the restart, in the same front end, as a checklist: steps this machine does not use are skipped without being shown, and it stops only for what needs you — one restart for Secure Boot enrolment, and the disk passphrase and PIN to bind the TPM. On an encrypted machine the last step writes the disk's header backup to a drive you pick, or downloads it. Finish removes the wizard. Details: docs/guides/install-graphically.md.
nix flake init -t github:OWNER/nixie#sitein a new repository.- Edit
site.nix: one entry per host withnixie.profile, the administrator and the security options; put guests inguests.nixand cache contents indata.nix. nix run github:OWNER/nixie#deploy -- --site . --host <name> root@<target>installs over SSH (kexec if the target is another Linux, the ISO as is), restarts the machine, waits for it and carries on with the rest of setup in your own terminal: the passphrase, the PIN, the Secure Boot restarts and Finish. An encrypted target is unlocked at its screen, or over SSH on port 2222 when remote unlock is on, before it comes back.- From then on,
nixie applyon the host: it commits hand edits in/etc/nixie/site, switches the system, and pushes the site back whennixie.site.repois set. Details: docs/guides/install-headlessly.md.
| server | desktop | |
|---|---|---|
| contains | incusd, nftables, the control panel, optional Tailscale, exporters, backup timer, the front panel | Hyprland, greetd, the shell, PipeWire, NetworkManager, Bluetooth, the chosen packages |
| never contains | a compositor, a browser or any desktop package (unless nixie.console.kiosk.enable, which adds exactly cage and the kiosk browser) |
Incus, OpenTofu, Prometheus, Grafana, restic, Cockpit (unless the site enables them) |
| proven by | closure checks | closure checks |
Both share the boot chain, the security stack, the installer and the
nixie command (docs/concepts/hosts.md).
| option | what it does | what it costs | default |
|---|---|---|---|
nixie.security.encryption.enable |
LUKS2 under ZFS on the system disk (and the data disk, keyed from the root) | a passphrase every boot; the one change that needs a reinstall | off (the ISO pre-selects on) |
nixie.security.tpm.enable |
an outer LUKS layer bound to the TPM 2.0 with a required PIN, PCRs from nixie.security.tpm.pcrs |
a PIN every boot; nixie reseal after firmware changes |
off |
nixie.security.secureBoot.enable |
lanzaboote signs the boot chain with your keys, enrolled from Setup Mode | a firmware visit; unsigned media will not boot | off |
nixie.security.attestation.enable |
a TOTP code from the TPM before the passphrase prompt | comparing a code at boot; nixie reseal after kernel updates |
off |
nixie.security.duress.enable |
a second passphrase that erases every key slot and powers off | no undo | off |
nixie.security.remoteUnlock.enable |
early-boot SSH with the administrator's keys | the early-boot host key on the boot partition | off |
nixie.security.lockdown |
kernel lockdown integrity mode | a kernel rebuild and no unsigned modules | none |
nixie.security.hardening.usbguard.enable |
USB allowlist from setup, plus usbguard.allow |
new devices blocked until nixie usb allow lists them |
on for servers |
Each was exercised in a VM with OVMF and swtpm (VERIFICATION.md#encryption); the table's costs are from docs/concepts/security.md.
# site.nix
{
hosts.server = {
hardware = ./hosts/server/hardware.nix; # written by the installer
secrets = ./secrets/server.yaml; # sops, read on the host at activation
guests = import ./guests.nix;
data = import ./data.nix;
settings = {
nixie.profile = "server";
nixie.auth.admin.name = "admin";
};
};
}One site holds as many machines as you like: another entry under hosts,
installed from the same ISO with this site's repository or with
nix run nixie#deploy. Every machine of the site is listed on the panel's
Machines page, on each of them.
# guests.nix: the only place guest names appear
{
web = {
recipe = "static-web";
ip = "192.0.2.10/24";
mounts."/data/state/web" = "/var/www";
backup = [ "/data/state/web" ];
expose.lan = [ 80 ];
module = ./guests/web/configuration.nix;
};
}# data.nix: what lives in cache/, by fetcher kind
{
http.dataset = { url = "https://example.invalid/dataset.tar"; sha256 = "…"; };
hf.model = { repo = "org/model"; rev = "main"; };
}The example site in examples/site declares one guest per capability
(native service, OCI image under podman, GPU, nesting, VM, foreign image
with cloud-init, fixed address, mounts, backups, tailnet exposure). The
checked snippet below lists them:
test -f site.nix && test -f guests.nix && test -f data.nix
grep -c '= {' guests.nix | grep -qx 7
grep -q 'recipe = "static-web"' guests.nix
grep -q 'kind = "vm"' guests.nix
grep -q 'nesting = true' guests.nix
grep -q 'gpu = true' guests.nix| command | does |
|---|---|
nixie apply [--yes] |
commits hand edits in /etc/nixie/site, host switch, guest images by hash, tofu apply, then pushes the site to nixie.site.repo when set; shows a plan first (VERIFICATION.md#guests) |
nixie site key |
the host's key for the site repository: give it write access there and apply pushes every change |
nixie fetch |
fills cache/ from data.nix; idempotent (VERIFICATION.md#data) |
nixie backup now|list|verify|kit <file> |
run a backup, list snapshots, check the repository, write the disaster kit (VERIFICATION.md#slice-m-backups) |
nixie restore <snapshot> [--path <p>] [--to <dir>] |
puts state/ (or one path) back in place, or beside the live data with --to |
nixie apply --confirm-within 10m |
as above, and reverts host and touched guests unless nixie apply --confirm arrives in time (VERIFICATION.md#slice-n-rollback) |
nixie rollback [--list | --generation N | --boot-previous] |
back to the previous system now, list generations (site commit, date, kernel), or boot the previous one next time |
nixie rollback guest <name> [--snapshot s] |
restore a guest's root disk from a snapshot; its state/ is untouched |
nixie rollback data <name> [--snapshot s] [--in-place] |
a state directory from a ZFS snapshot, beside the live one or in place |
nixie reseal |
reseals attestation to the running boot chain |
nixie security reenroll |
after a board, TPM or firmware change: Secure Boot enrolment, TPM + PIN binding with a new recovery key, attestation, lockout password, header backups; resumable, also from the front panel (e) (VERIFICATION.md#slice-o-recovery) |
nix run .#apply [<machine>] |
from a site checkout: copy it to that machine and run its own nixie apply there; with no machine, this one |
nixie update [--check | --now] |
whether the site repository is ahead of this machine, and apply it; the timer does this by itself (nixie.updates.mode) |
nixie update --inputs [name…] |
update what the site pins, the platform itself among them, then apply; the other machines follow it as they do any change |
nix run .#demo-shots [<dir>] |
every screen of the control panel in all three finishes, from demo mode: no daemon, no VM, which is what lets CI take them |
nix run .#offline |
archive every pinned input into a store of its own, then evaluate every output with the network refused: nothing fetches behind the lock file |
| the host page's Run panel | Apply the site, Fetch the cache, Run the checks, each streaming its output; the same commands, through Cockpit's bridge |
nixie notices |
what this machine wants you to know, as the front panel, the control panel, the host page and the desktop show it |
nixie secure-boot [--sign] [--at <dir>] |
what the firmware holds, what is staged and what is signed, when a start says "Access Denied"; --at reads a machine opened with nixie disk open from the installer image, for one that will not start at all |
nixie security add-key |
enrol another FIDO2 security key for the disk (a spare); asks for the passphrase, the key's PIN and a touch |
nixie disk open [<partition>] [--mount <dir>] [--write], nixie disk close |
open another Nixie disk, on the installer image or another machine, with a security key, the recovery key or the passphrase, its pools under temporary names at /mnt/nixie and that disk's boot partition at /mnt/nixie/boot, read-only unless --write |
nixie rollback --json |
every generation, guest and data snapshot and backup as JSON; the History screens read this |
nixie hardware scan | refresh |
compare the machine with hosts/<name>/hardware.nix, then rewrite it, commit and apply |
nixie hardware add-disk <by-id> [name] |
format and mount a disk the site does not declare; it refuses any that it does |
nixie usb [--json], nixie usb allow <vendor:product[/serial]> |
blocked USB devices; allow one in hosts/<name>/usb.nix and commit, then nixie apply |
nixie doctor |
TPM, attestation, Secure Boot, key slots, whether the last unlock needed the recovery key, blocked USB devices, guests, backup check, disk space |
nixie report [<file> | -] |
one file for a bug report: disks, firmware, network, setup's log, this and the last start's journal, failed services and doctor, with recovery, age and private keys blanked; on the installer too, and a failed wizard step offers the same file |
nixie export <instance> |
a guests.nix entry for a scratch instance |
nixie declare <instance> |
write that entry into the site's guests.nix and apply; the instance keeps running, and the apply adopts it instead of making a second one. The control panel's Declare opens the host page to run this |
nixie menu |
the desktop menu: finish, wallpaper, packages, update, keybinds |
Every machine of a site follows the same git repository (nixie.site.repo).
nixie apply on one of them commits what you changed, applies it and pushes
it; the others notice and, depending on nixie.updates.mode, either say so
or catch up:
| mode | what a machine does when the repository moves ahead |
|---|---|
notify (the default) |
says so on the front panel, the control panel, the host page, a desktop notification and at login, and waits for nixie update --now |
auto |
applies it by itself, then confirms only if nixie doctor comes out no worse than before; otherwise the machine puts itself back (nixie.updates.confirmWithin) |
off |
does not look |
Applying is nixie apply, so a change reaches the host and the guests the
site declares in the same step: a server's instances and a desktop's own
configuration follow the same way. nixie.updates.schedule sets how often to
look (hourly by default), and a machine that was off looks as soon as it is
back.
The platform travels the same way: nixie update --inputs (or the desktop
menu's "Update inputs") updates what the site's lock pins, applies it and
pushes it, and the other machines see a newer site like any other change.
What the kit plus the site repository give back, and the order:
- Install from the ISO (either quick start), cloning the site repository
on the Name step, and give that step
age.keyfrom the kit (the headless deploy asks for its path) so the secrets the site already holds for this machine decrypt again. - Put
restic-password(andrestic-envorrclone.confif the kit has them) back as the site's sops secrets fornixie.backups. - On the new machine:
nixie apply, thennixie restore latest. Guests come back fromguests.nixwith their restoredstate/;cache/is refetched withnixie fetch. headers/in the kit restores a damaged LUKS header (cryptsetup luksHeaderRestore <device> --header-backup-file <name>.header);recovery-key.txtopens the TPM layer when the TPM cannot. The key from setup was shown once and is kept nowhere on the host; after using it,nixie security reenrollrebinds the TPM and shows a new one.
nixie backup kit <file> writes the kit, encrypted with a passphrase you
type; keep it offline. Local ZFS snapshots (nixie.backups.snapshots, and
one before every apply) cover mistakes on a healthy disk; restic covers
losing the disk.
https://<host>:8443/ui/ is the Incus daemon serving the panel: instances
with their terminal, files, logs, snapshots and metrics, images, profiles,
networks, storage, operations, dashboards and guest history. Machines lists
every machine in the site, not only the one serving the page, and opens any
other machine's panel; the list is written from site.nix by lib.mkSite,
so a machine added to the site appears there after the next nixie apply.
A browser gets in with a client certificate the daemon trusts; the panel's first page gives
the commands. The gear in the header opens Settings: the finish, the figures
in the header, the time range a browser starts with, which Overview panels
show and in what order and width, the pages in the navigation and extra
links. They are kept on the host for every browser, in the daemon's
user.nixie.ui setting, and start from nixie.ui.theme and
nixie.ui.links
(VERIFICATION.md#a-server-installed-through-the-web-wizard).
The panel, the installer and the machine's own screens share one set of
tokens and one look: the same three finishes, the same controls, and motion
that stops when the browser asks for reduced motion.
The first text console shows a front panel (nixie.console.frontPanel.enable,
on by default); any key opens the normal login. nixie.console.kiosk.enable
keeps a kiosk with the control panel behind a lock page on the local display
(docs/console.md, VERIFICATION.md#console).
The desktop profile is a complete Hyprland rice, declared in the site and
switchable at runtime (docs/desktop.md,
VERIFICATION.md#slice-r-desktop-rice):
a floating bar (mark, workspaces, focused window or playing track, tray,
processor, memory and temperature readouts, network, Bluetooth, volume,
battery, clock), a launcher with app icons and favourites plus file,
calculator, emoji and clipboard modes, a notification centre with
do-not-disturb, a control centre (sliders, toggles, media, the finish
swatches, wallpaper), lists that join a Wi-Fi network, connect a paired
device and set the volume of each playing app, a screenshot menu, a
keep-awake inhibitor, a calendar, OSDs, a tiled power menu, a window
switcher, a wallpaper picker, a keybind cheat-sheet, a themed lock screen,
kitty with a starship prompt and fastfetch. Hyprland is configured
in Lua from nixie.desktop.* (look.gaps, look.rounding, look.blur,
look.animations, fonts.ui, favourites, workspaces.labels and the
rest); ~/.config/hypr/local.lua is loaded last. Super+T cycles the
finish and Super+W the wallpaper for the session with no rebuild;
nixie.desktop.finish stays the declared default.
Themes are data. nixie.desktop.themes adds finishes of your own from a
handful of colours, and nixie.desktop.hyde.themes imports a HyDE theme
directory straight from its own files, so HyDE's themes and wallpapers work
with the Nixie shell, pinned and offline. With accentFromWallpaper the
accent follows the wallpaper. If you would rather run HyDE itself,
nixie.desktop.hyde.enable (a choice on the installer's Desktop step) brings in
the real HyDE through the hydenix flake and keeps the same installer, security
options and nixie command.
nixie.hostUi.enable adds Cockpit with the files, terminal, storage and
podman plugins, in the same finish as the control panel, behind the
administrator password and the second factor from nixie.auth.secondFactor
(TOTP). It is off because it widens the attack surface of a hardened host
(VERIFICATION.md#host-page).
Every hook point is an option a site sets: docs/extending.md. The option reference is generated from the module tree: docs/reference/options.md.
ARCHITECTURE.md: layout, the option tree, the site contract, the phase engine, the installer state machine, the desktop shell choice, and every deviation from the brief with its reason.
Version 0.1.0, unreleased. By section of the brief:
| section | state |
|---|---|
| 3 platform flake, site flake, template | done |
| 4 option tree | done (VERIFICATION.md#slice-a) |
| 5 guests: schema, derivations, declared/scratch, Export | done; Declare from the panel waits for a host agent |
| 6 data and manifest, backups, restore | done; change request: local snapshots, verify, kit, restore beside (VERIFICATION.md#slice-m-backups) |
| 11 rollback (change request) | done: generations kept and labelled, nixie rollback, apply --confirm-within, front-panel rollback (VERIFICATION.md#slice-n-rollback) |
| 7 recovery (change request) | done: recovery key shown once and never stored, unlock falls back to it, nixie security reenroll, nixie usb, keyboards allowed during setup and reenroll (VERIFICATION.md#slice-o-recovery) |
| 7 security features | done; lockdown integrity is an option that rebuilds the kernel and is documented, not runtime-verified |
| 8 networking, exit-node egress | done for managed-nat; exit-node needs managed-nat by design |
| 9 monitoring and backups | done |
| 10 control panel | done for every screen the brief lists; screens the design does not draw follow its recipes |
| 11 installer: kiosk, LAN, headless, setup generation | done; the headless path is verified through the same phase scripts, not a full kexec run |
| 12.1 server host page | done (TOTP; no passkeys, see ARCHITECTURE D3) |
| 12.2 desktop | done; overview is the shell's window panel (D29); change request: the full rice, Lua config, runtime finish and wallpaper switching, network, device, volume and screenshot menus, system readouts, keep-awake, wallpaper-derived accent, site and HyDE themes (VERIFICATION.md#slice-r-desktop-rice) |
| 13 extension points | done |
| 14 docs, examples | done; the gallery above is regenerated by nix run .#media |
| console slice | done |
| showcase slice | done: the gallery comes from media runs, this README is checked |
- Cluster mode: several hosts in one site sharing guests and storage.
- A native host agent, which would also enable Declare from the panel and passkeys for the host page.
nix flake check -L must be green before a commit; every VM test has to have
run here. Add a VERIFICATION.md entry with what you ran. Comments say why,
never what; nothing hardware- or user-specific goes into the platform.
MIT. Bundled fonts: Archivo and JetBrains Mono under the SIL Open Font License.




















