A reproducible NixOS deployment system for multi-PC environments (classrooms, training rooms, public labs, libraries) with no internet access on client machines.
One controller PC manages the entire lab: it builds all configurations locally, serves them over LAN, and deploys updates to every workstation declaratively. The whole lab can be reinstalled from scratch in under 20 minutes.
Managing a multi-PC lab is painful. Machines drift over time, reinstalling by hand is slow and error-prone, and keeping many systems consistent is a full-time job. Traditional tools like Ansible help, but they can't guarantee that two machines built a week apart end up identical.
NixOS solves this with declarative, reproducible configurations -- but most NixOS workflows assume internet access. In many schools, offices, and public labs, client PCs either have no internet at all or only get access after a user logs into an institutional network.
This project bridges that gap with a local-first workflow:
- A single controller PC acts as the build server, binary cache, and PXE boot server
- Client PCs are installed and updated entirely over the LAN
- One
flake.nixfile is the single source of truth for the entire lab - Everything is parameterizable -- user names, passwords, network settings, locale -- so you can fork this repo and adapt it to your environment in minutes
- Zero-internet client installation -- PXE/netboot + local binary cache (Harmonia), no USB drives needed per client
- Single source of truth -- one
flake.nixgenerates all host configurations programmatically - Multi-machine orchestration -- deploy updates to all PCs at once with Colmena
- Student home directory reset -- homes are restored to a clean template on every boot, with the last 5 sessions saved as Btrfs snapshots for recovery
- Classroom management -- Veyon is pre-configured with all lab PCs mapped, ready to use
- Fully parameterizable -- user names, PC count, network layout, passwords, locale, homepage, and more are all configurable from a single settings block
- Dual networking -- DHCP for institutional network integration + static IPs for the internal lab network
- UEFI + Btrfs -- modern boot with declarative disk partitioning (Disko) and snapshot support
- GNOME desktop -- pre-configured with dark theme, development tools, and terminal customization
- Controller as teacher workstation -- the controller is intended for instructor use and shows a user chooser at login (no autologin)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Controller (pcNN) β
β β
β Nix Flake βββΊ Build all configs βββΊ Harmonia (binary cache) β
β PXE/Netboot server β
β Colmena orchestration β
ββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββ
β LAN (static IPs)
ββββββββββββββββΌβββββββββββββββ
β β β
ββββββΌβββββ ββββββΌβββββ ββββββΌβββββ
β pc01 β β pc02 β β pcNN β
β Student β β Student β β Student β
βWorkstat.β βWorkstat.β βWorkstat.β
βββββββββββ βββββββββββ βββββββββββ
| Component | Description |
|---|---|
| Controller | PXE/Netboot server, local binary cache, Colmena orchestration |
| Client PCs | Student workstations with Btrfs snapshots and home reset |
| Networking | Installation and updates over LAN only, no internet required on clients |
| Boot mode | UEFI only, declarative partitioning with Disko |
Optional: fork first. If you want to push your
lab-config.nixto a remote repository (for backup or future reinstalls), fork this repo on GitHub before starting. The main flow below works with a plaingit cloneof the original repo -- forking is not required.
Requires: a NixOS live USB with temporary internet access. UEFI boot must be enabled.
Boot the controller PC from the NixOS live USB, then run:
curl -fsSL https://raw.githubusercontent.com/giovantenne/nixos-lab/master/scripts/install-controller.sh | bashIf one disk is detected, the script selects it automatically; if multiple disks are detected, it asks you to choose one.
This installs the controller with default placeholder settings from lab-config.nix. SSH, binary cache, and Veyon public keys are added later, after step 4.
The bootstrap script forces cache.nixos.org during installation, so it does not depend on any LAN cache or substituter already configured in the live environment.
Using your own fork? Only one env var is needed:
curl -fsSL https://raw.githubusercontent.com/YOUR_USER/nixos-lab/master/scripts/install-controller.sh | \ FLAKE_REF="github:YOUR_USER/nixos-lab" bash
Reboot and log in as admin (default password: nixos).
git clone https://github.com/giovantenne/nixos-lab.git
cd nixos-labIf you forked the repo, clone your fork instead:
git clone https://github.com/YOUR_USER/nixos-lab.git
Now you have all the values you need. Find your DHCP address and interface name:
ip -4 addrGenerate the password hashes before filling the three password fields below. The default password for all users is nixos:
# Hashed passwords (run once per user, paste each hash into lab-config.nix)
mkpasswd -m sha-512Edit lab-config.nix with your lab's settings:
# ββ Network ββββββββββββββββββββββββββββββββββββββββββββββββββββ
masterDhcpIp = "MASTER_DHCP_IP"; # DHCP address of controller (from ip -4 addr)
networkBase = "10.0.0"; # First 3 octets of static lab subnet
pcCount = 20; # Number of student PCs
masterHostNumber = 99; # Controller PC number
ifaceName = "enp0s3"; # Network interface name (from ip -4 addr)
# ββ User accounts βββββββββββββββββββββββββββββββββββββββββββββ
teacherUser = "teacher"; # Teacher account name
studentUser = "student"; # Student account name
# ββ Passwords (SHA-512 hashed) ββββββββββββββββββββββββββββββββ
# Default is "nixos" for all accounts. Generate your own with: mkpasswd -m sha-512
teacherPassword = "...";
studentPassword = "...";
adminPassword = "...";
# ββ School / organization βββββββββββββββββββββββββββββββββββββ
homepageUrl = "https://github.com/giovantenne/nixos-lab";
# ββ Locale / timezone βββββββββββββββββββββββββββββββββββββββββ
timeZone = "Europe/Rome";
defaultLocale = "en_US.UTF-8";
extraLocale = "it_IT.UTF-8";
keyboardLayout = "it";
consoleKeyMap = "it2";You can leave the git identity fields at their defaults for now.
Note:
masterDhcpIpis used only during PXE/netboot client installation. The generated iPXE script, the netboot ramdisk, and the PXE helper services all point to that DHCP address, so if the DHCP lease changes before a netboot session you must updatelab-config.nixand rebuild the netboot artifacts. Regular Colmena deploys use the controller's static lab IP instead.
Generate the three required key pairs. Keep the private files local to the controller, and add the public files to Git in the last command below so Nix can read them from the repo.
| Key pair | Private file | Public file / config | Purpose |
|---|---|---|---|
| Binary cache | secret-key |
public-key (generated locally, committed) |
Harmonia signs Nix store paths; clients verify signatures |
| SSH | id_ed25519 |
id_ed25519.pub (generated locally, committed) |
Admin SSH access + Colmena deploys (connects as root) |
| Veyon | veyon-private-key.pem |
veyon-public-key.pem (generated locally, committed) |
Veyon Master authenticates to student PCs |
Generate everything from scratch:
# Binary cache signing key for Harmonia
nix key generate-secret --key-name lab-cache-key > secret-key
nix key convert-secret-to-public < secret-key > public-key
# Admin SSH key used by Colmena / SSH access
ssh-keygen -t ed25519 -f id_ed25519 -N '' -C 'admin@controller'
# Veyon RSA keypair
openssl genrsa -out veyon-private-key.pem 4096
openssl rsa -in veyon-private-key.pem -pubout -out veyon-public-key.pem
# SSH private key -- used by Colmena to connect as root to all PCs
install -m 600 -D id_ed25519 ~/.ssh/id_ed25519
# SSH public key -- useful for normal SSH tooling; keep a copy in the repo root too
install -m 644 -D id_ed25519.pub ~/.ssh/id_ed25519.pub
# Veyon private key -- only needed on the controller (where Veyon Master runs)
# Only users in the veyon-master group (admin + teacher) can read it
sudo install -d -m 0750 -g veyon-master /etc/veyon/keys/private/teacher
sudo install -m 0640 -g veyon-master veyon-private-key.pem /etc/veyon/keys/private/teacher/key
# Flakes ignore untracked files in a Git worktree, so add the public files
git add public-key id_ed25519.pub veyon-public-key.pemBefore starting PXE, temporarily remove the controller's static lab IP from the shared interface. The generated netboot artifacts refer to masterDhcpIp, so this keeps PXE, HTTP, and binary-cache traffic on that single DHCP address during installation. The change is temporary and a reboot restores the static IP automatically.
# Rebuild the controller with your real config
sudo nixos-rebuild switch --flake .#$(awk '/masterHostNumber =/ { gsub(/[^0-9]/, ""); print "pc" $0; exit }' lab-config.nix) --no-write-lock-file
# Build netboot artifacts
nix build .#nixosConfigurations.netboot.config.system.build.kernel --out-link result-kernel
nix build .#nixosConfigurations.netboot.config.system.build.netbootRamdisk --out-link result-initrd
nix build .#nixosConfigurations.netboot.config.system.build.netbootIpxeScript --out-link result-ipxe
# Install iPXE bootstrap binary
nix build nixpkgs#ipxe --out-link result-ipxe-bin
install -D -m 0644 result-ipxe-bin/snp.efi assets/ipxe/snponly.efi
# Pre-build all client closures
PC_COUNT=$(awk '/pcCount =/ { gsub(/[^0-9]/, ""); print; exit }' lab-config.nix)
TARGETS=()
for i in $(seq 1 "$PC_COUNT"); do
TARGETS+=(".#nixosConfigurations.pc$(printf "%02d" "$i").config.system.build.toplevel")
done
nix build "${TARGETS[@]}"
# Temporarily remove the lab static IP so netboot uses masterDhcpIp only
STATIC_IP=$(awk -F'"' '/networkBase =/ { print $2; exit }' lab-config.nix).$(awk '/masterHostNumber =/ { gsub(/[^0-9]/, ""); print; exit }' lab-config.nix)
IFACE=$(awk -F'"' '/ifaceName =/ { print $2; exit }' lab-config.nix)
sudo ip addr del "${STATIC_IP}/24" dev "${IFACE}"Open two separate terminals:
Terminal 1 -- Binary cache:
./scripts/run-harmonia.shTerminal 2 -- ProxyDHCP + TFTP + HTTP netboot server:
sudo ./scripts/run-pxe-proxy.shBoth processes run in the foreground. Keep the terminals open during client installation.
On each client PC, enable UEFI network boot in the BIOS/firmware settings. The PC will PXE-boot into a NixOS ramdisk environment.
On the booted client:
/installer/setup.sh XXWhere XX is the PC number (e.g., /installer/setup.sh 5 for pc05).
setup.shauto-selects the disk if only one is present; if multiple disks are detected, it asks for a choice.
When all clients are installed, restore the controller's static lab IP so Colmena can reach the lab subnet again (or just reboot it):
STATIC_IP=$(awk -F'"' '/networkBase =/ { print $2; exit }' lab-config.nix).$(awk '/masterHostNumber =/ { gsub(/[^0-9]/, ""); print; exit }' lab-config.nix)
IFACE=$(awk -F'"' '/ifaceName =/ { print $2; exit }' lab-config.nix)
sudo ip addr add "${STATIC_IP}/24" dev "${IFACE}"First apply the latest configuration on the controller itself:
sudo nixos-rebuild switch --flake .#$(awk '/masterHostNumber =/ { gsub(/[^0-9]/, ""); print "pc" $0; exit }' lab-config.nix) --no-write-lock-fileThen start the binary cache:
./scripts/run-harmonia.shDeploy to all lab PCs:
nix run nixpkgs#colmena -- apply --impure --on @labDeploy to a single PC:
nix run nixpkgs#colmena -- apply --impure --on pc05Use nixos-rebuild only on the machine you are rebuilding.
Rebuild the controller locally:
sudo nixos-rebuild switch --flake .#$(awk '/masterHostNumber =/ { gsub(/[^0-9]/, ""); print "pc" $0; exit }' lab-config.nix) --no-write-lock-fileFor client PCs, prefer Colmena from the controller. Only run sudo nixos-rebuild switch --flake /path/to/nixos-lab#pc05 --no-write-lock-file after logging into pc05 itself (or after cloning the repo there).
| User | Role | Details |
|---|---|---|
admin |
System administrator | SSH access, sudo, Veyon Master access |
| Teacher (configurable) | Instructor | Veyon Master access, persistent home, snapshot bookmark |
| Student (configurable) | Student | Autologin on client PCs, home reset at every boot |
root |
System | Password disabled, SSH key access only |
All machines must boot in UEFI mode. Disk partitioning is declarative via disko-uefi.nix, which wraps the shared pure layout in lib/disko-layout.nix.
| Partition | Filesystem | Mount point |
|---|---|---|
| EFI System Partition | FAT32 | /boot |
| Root partition | Btrfs | -- |
Btrfs subvolumes:
| Subvolume | Mount point |
|---|---|
@root |
/ |
@home-<studentUser> |
/home/<studentUser> |
@snapshots |
/var/lib/home-snapshots |
The student home directory resets to a clean template on every boot:
- Template: generated at activation time with git config, VS Code settings and extensions, and XDG directories
- Snapshots: the last 5 sessions are saved in
/var/lib/home-snapshots/(accessible byadmin, the teacher user, androot)
The teacher user has a Snapshots bookmark in the Nautilus sidebar.
To recover student work from a previous session:
ls /var/lib/home-snapshots/snapshot-1/
cp /var/lib/home-snapshots/snapshot-1/file.txt /home/<studentUser>/Veyon is packaged locally (not in nixpkgs) and deployed on all PCs. The
veyon-service systemd unit runs on every machine, accepting connections on
port 11100.
- All PCs have
veyon-servicerunning and the public key deployed via Nix Veyon.confis generated with all lab PCs pre-mapped under the hardcoded location nameLab- The Veyon private key is only needed on the controller -- student PCs only have the public key
- Users in the
veyon-mastergroup (adminand the teacher user) can access Veyon Master
The default desktop is GNOME (Wayland) with a curated set of development tools. To customize:
- System packages: edit the
environment.systemPackageslist inmodules/common.nix - GNOME settings: edit the
extraGSettingsOverridesinmodules/common.nix - Screensaver: replace
assets/logo.txtwith your own ASCII art - Wallpapers: replace images in
assets/backgrounds/ - VS Code extensions: edit the
vscodeExtensionslist inmodules/home-reset.nix
flake.nix # Entry point: host generation, Colmena config, labMeta export
flake.lock # Pinned inputs (nixpkgs, disko)
LICENSE # MIT license
lab-config.nix # Lab configuration (edit for your environment)
disko-uefi.nix # NixOS wrapper for the shared Disko layout
lib/
disko-layout.nix # Shared Disko layout function (device + student user)
setup.sh # Client PC installer (runs on PXE-booted machines)
pkgs/
veyon.nix # Veyon package derivation
gnome-remote-desktop.nix # gnome-remote-desktop overlay (VNC + multi-session)
modules/
common.nix # GNOME desktop, packages, shells, locale, services
hardware.nix # Generic hardware detection
networking.nix # Hostname + static IP per host
users.nix # User accounts and autologin
cache.nix # Binary cache client configuration
filesystems.nix # Btrfs support
home-reset.nix # Student home templating + boot-time reset
veyon.nix # Veyon service, keys, and classroom config
scripts/
install-controller.sh # Controller bootstrap from live USB
run-harmonia.sh # Binary cache server
run-pxe-proxy.sh # ProxyDHCP + TFTP + HTTP netboot server
lib/lab-meta.sh # Shared helper: loads labMeta from the flake
cmd-screensaver.sh # TTE screensaver animation loop
launch-screensaver.sh # Fullscreen Ghostty screensaver launcher
screensaver-monitor.sh # GNOME idle watcher for screensaver
create-home-template.sh # Home directory template builder
home-reset.sh # Boot-time snapshot rotation + home reset
assets/
backgrounds/ # Wallpapers (randomly selected at home reset)
logo.txt # ASCII art for screensaver
mimeapps.list # Default applications
vscode-settings.json # VS Code defaults
Public key artifacts generated during setup live in the repo root; see step 4.
- Never commit
secret-key,id_ed25519, orveyon-private-key.pem(all are in.gitignore) - Commit only the public counterparts used by the Nix configuration:
public-key,id_ed25519.pub, andveyon-public-key.pem - Passwords are SHA-512 hashed; never store plaintext
- SSH password authentication is disabled; key-based only
users.mutableUsers = falseenforces declarative user management- The Veyon private key is readable only by the
veyon-mastergroup
Released under the MIT License.