octocat/minimal-nixos
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
-
Put
out/nixos-vultr-*.isosomewhere reachable by URL and add it in the Vultr panel under Orchestration > ISOs > Add ISO. -
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.
-
Open the web console. It auto-logs in as root. Run:
vultr-install # partitions /dev/vda, copies the closure, installs GRUBPass another device if the disk is not
/dev/vda(virtio-scsi plans expose/dev/sda). -
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 = falseand drop the/bootfilesystem to save ~30 MB. - git-minimal + vim (~95 MB): convenience. Remove them from
environment.systemPackagesif you do not need an editor or git on the box (nanois disabled, so re-enable it or keep vim). - nix (~90 MB with boost/icu): needed to rebuild the server in place.