Skip to content

octocat/minimal-nixos

octocat /

Read-only mirror — pushes are disabled.

MINIMAL-NIXOS

A deliberately small, text-only NixOS configuration for a generic server VM on Vultr, plus a custom installer ISO that puts it on the instance's disk with no network access needed during the install.

Closure of the installed system: ~900 MiB. Installer ISO: ~500 MB (the ISO also holds the installer's own live system and is zstd-compressed).

Layout

File Purpose
flake.nix Follows nixpkgs nixos-unstable (was nixos-26.05 until 2026-09-03); exposes server and installer configs and the iso package
modules/server.nix The system that gets installed. Add services here or in a new module
modules/installer.nix Live ISO carrying the prebuilt server closure and a vultr-install script
build.sh Syncs the repo to the NixOS dev VM and builds there (the ISO is x86_64; the VM builds it via binfmt emulation)

Build

The repo is built on the dev VM in /home/realo/Data/Work/MINIMAL-SERVER:

VM_PASS=... ./build.sh eval # evaluate only, catches option errors fast
VM_PASS=... ./build.sh server # build the server closure and print its size
VM_PASS=... ./build.sh iso # build the ISO and copy it to ./out/
VM_PASS=... ./build.sh test # boot the ISO in QEMU on the VM, install, reboot, log in

To do the same by hand on the VM (serial console, you type the commands):

test/qemu-manual.sh install # boots the ISO; run vultr-install, then poweroff
test/qemu-manual.sh boot # boots the installed disk; login realo, or ssh -p 2222
UEFI=1 test/qemu-manual.sh ... # same, but firmware is OVMF instead of SeaBIOS
test/qemu-manual.sh reset # throw the disk image away

test runs test/qemu-boot-test.sh: an expect script that boots the ISO on an 8 GB virtio disk under QEMU (TCG, so it is slow but works on the aarch64 VM), runs vultr-install, reboots from the disk and logs in as realo over the serial console. It prints PASS or a FAIL: reason.

Without VM_PASS, plain key-based ssh is used. VM_HOST and VM_DIR override the defaults.

Deploy on Vultr

  1. Put out/nixos-vultr-*.iso somewhere reachable by URL and add it in the Vultr panel under Orchestration > ISOs > Add ISO.

  2. Deploy a Cloud Compute instance and pick the ISO under Custom ISO. The ISO is hybrid (BIOS + UEFI); the installed GRUB is hybrid too, so either boot mode works.

  3. Open the web console. It auto-logs in as root. Run:

    vultr-install # partitions /dev/vda, copies the closure, installs GRUB

    Pass another device if the disk is not /dev/vda (virtio-scsi plans expose /dev/sda).

  4. Detach the ISO in the panel and reboot.

Then ssh realo@<ip> or ssh root@<ip> with a key from sshKeys in modules/server.nix. There are no passwords at all: SSH is key-only and sudo asks for none. The Vultr VNC console therefore cannot log in to the installed system; boot the ISO again if you need a rescue shell. To allow console login, set users.users.realo.hashedPassword to the output of openssl passwd -6.

Updating the server

The flake is copied to /etc/nixos at install time. On the server:

cd /etc/nixos && vim modules/server.nix
rebuild # = nixos-rebuild switch --flake /etc/nixos#server
rebuild boot # activate on next reboot instead

rebuild is a small shell script defined in server.nix. The real nixos-rebuild is disabled because in nixpkgs 26.05 it is a Python program that adds ~130 MB; set system.tools.nixos-rebuild.enable = true to get it back.

To sync from this repo instead, git clone it to /etc/nixos (git is installed) and run rebuild.

Evaluating the config takes ~750 MB (230 MB resident, the rest swappable). server.nix therefore declares a 2 GB swap file that NixOS creates at boot; with it, rebuild takes ~2.5 min cold and ~40 s once nixpkgs is in the store, even on a 512 MB instance.

Automatic upgrades: the nixos-upgrade timer (04:40 UTC daily, plus up to 30 min of jitter) runs nix flake update nixpkgs in /etc/nixos, then rebuild boot. If the kernel, modules or initrd changed it reboots one minute later, otherwise it switches live. A garbage collection follows every successful run. Store maintenance: nix.gc daily (--delete-older-than 2d), nix.optimise weekly, auto-optimise-store on write, GRUB keeps 5 entries. Watch it with journalctl -u nixos-upgrade and systemctl list-timers.

Backups: a role module can drop timestamped archives into /var/backups (the headscale module does, daily). SERVER=root@<ip> ./build.sh backup pulls that whole directory to ~/Backups/<hostname> on the Mac (or $BACKUP_DIR) over ssh; the server cannot reach the Mac, so run it by hand or from a launchd job.

Deploying from the dev VM is still the way to push config changes. It uses nixos-rebuild --target-host, which builds the x86_64 closure there (binfmt emulation), copies it with nix copy and activates it over SSH:

SERVER=root@<ip> VM_PASS=... ./build.sh deploy # switch now
SERVER=root@<ip> VM_PASS=... ./build.sh deploy boot # activate on next reboot

Or directly on the dev VM, in the repo directory:

nixos-rebuild switch --flake .#server --target-host root@<ip>

The dev VM's own SSH key is in sshKeys for this purpose. build.sh deploy also refreshes /etc/nixos on the server with the deployed sources (a tar copy of the same file set as packages.src), which the nightly upgrade and rebuild read. That copy includes flake.lock, so before deploying, bring the repo's lock up to the server's (scp root@<ip>:/etc/nixos/flake.lock .) or run nix flake update; otherwise the deploy steps back to the older nixpkgs until the next nightly run.

What was cut, and why

Cut Saves How
linux-firmware and all firmware blobs ~700 MB hardware.enableRedistributableFirmware = false, hardware.firmware = []
nixpkgs source in the closure ~250 MB nixpkgs.flake.setNixPath/setFlakeRegistry = false
Python (via nixos-rebuild-ng) ~130 MB system.tools.nixos-rebuild.enable = false, replaced by rebuild
Manuals, man pages, info, nixos-option (man-db, groff) ~60 MB documentation.*, system.tools.nixos-option.enable = false
QEMU guest agent (qemu-ga, glib) ~20 MB services.qemuGuest.enable = false
systemd-importd (gnupg, openldap) ~17 MB systemd.services.systemd-importd.enable = false
perl default packages, LVM, udisks2, fontconfig, xdg, containers ~30 MB profiles/minimal.nix, services.lvm.enable = false, boot.enableContainers = false
All locales except en_US ~200 MB i18n.supportedLocales
Every kernel module in the initrd except virtio + sd_mod initrd size boot.initrd.availableKernelModules

What is still there and why:

  • Kernel modules (~145 MB): the stock kernel's full module tree. Trimming it means building a custom kernel, which is very slow under emulation on the dev VM. Only virtio modules are loaded at boot; the rest just sit on disk.
  • perl (~55 MB): NixOS activation and the GRUB installer are Perl scripts. Going perl-less requires systemd-boot, which is UEFI-only.
  • Two GRUB builds (~60 MB): one BIOS, one EFI, for the hybrid boot loader. If you know the instance boots in legacy mode, set efiSupport = false and drop the /boot filesystem to save ~30 MB.
  • git-minimal + vim (~95 MB): convenience. Remove them from environment.systemPackages if you do not need an editor or git on the box (nano is disabled, so re-enable it or keep vim).
  • nix (~90 MB with boost/icu): needed to rebuild the server in place.