Day 62 — Images & nixos-generators
Day 62 — Images & nixos-generators
Stage VI · ~4h
Goal: Build a bootable artifact (cloud image, ISO, or VM format) from your flake modules using nixos-generators (and related patterns), then boot it once and prove it is the same module graph as a live host—not a snowflake fork.
Images are first-class deploy targets in Stage VI, not a side quest. The win is one module tree → install path and golden image path. NixOS 26.05 pin stays consistent with the rest of this volume.
Why this day exists
Not every node is nixos-install by hand. Cloud and edge want images: qcow2, AMIs (via pipelines), raw disks, installer ISOs. Generating images from the same modules as deploy targets closes a huge consistency gap—and catches “works on my VM but not in the AMI” earlier.
Without an image story you either:
- Hand-maintain cloud user-data that reimplements half of NixOS, or
- Drift image hosts away from Colmena/deploy-rs nodes until nobody trusts either.
Theory 1 — Image = NixOS config + format
same modules as hosts/edge
│
▼
nixos-generators / make-disk-image / sd-image
│
▼
artifact (iso, qcow2, raw, gce, azure, …)
| Format (examples) | Use |
|---|---|
iso |
Installer / live recovery |
qcow / qcow2 / vmware / virtualbox |
Hypervisors |
amazon / gce / azure |
Cloud (tooling evolves) |
sd-card / raw |
SBC / dd |
Check the current nixos-generators format list for your pin—names and defaults change. Prefer the project README over stale blog posts.
Mental model
An image build is still a NixOS evaluation: modules merge, activation scripts exist, store paths land on a disk layout the format owns. You are not “exporting files”; you are realizing a system derivation into a disk picture.
Theory 2 — flake + nixos-generators
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
inputs.nixos-generators.url = "github:nix-community/nixos-generators";
inputs.nixos-generators.inputs.nixpkgs.follows = "nixpkgs";
outputs = { self, nixpkgs, nixos-generators, ... }: {
packages.x86_64-linux.edge-qcow = nixos-generators.nixosGenerate {
system = "x86_64-linux";
format = "qcow";
modules = [
./modules/profiles/baseline.nix
./hosts/edge/image.nix
];
};
packages.x86_64-linux.lab-iso = nixos-generators.nixosGenerate {
system = "x86_64-linux";
format = "iso";
modules = [ ./hosts/lab-iso.nix ];
};
};
}nix build .#edge-qcow -L
ls -lh result*
# often result is a symlink to the image file or a directory containing itfollows discipline
Always pin generators’ nixpkgs to your flake’s nixpkgs. Mixed pins produce “image builds but host rebuilds something else” headaches that look like ghosts.
Theory 3 — Image-specific config
Images need first-boot realities: growpart, cloud-init or pure Nix alternatives, SSH keys baked or injected, serial console, predictable hostnames.
# hosts/edge/image.nix
{ modulesPath, pkgs, ... }:
{
imports = [
# sometimes:
# (modulesPath + "/profiles/qemu-guest.nix")
];
services.openssh.enable = true;
users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAA… image-lab"
];
# Disallow password auth even on images
services.openssh.settings.PasswordAuthentication = false;
networking.hostName = "edge-image";
system.stateVersion = "26.05";
# Smaller images: careful with documentation packages
documentation.nixos.enable = false;
documentation.man.enable = false;
}| Concern | Image habit |
|---|---|
| SSH access | Baked lab key or cloud metadata; never password-only |
| Disk grow | Guest agents / cloud-init / nixos grow modules |
| Console | Serial-friendly; qemu-guest profiles when KVM |
| Hostname | Static lab name vs cloud-init rewrite—pick deliberately |
| Docs packages | Off for edge images; on for installer ISOs if you want man pages |
Cloud-init vs pure NixOS images is a design choice; lab can use baked keys and skip cloud-init entirely.
Theory 4 — Consistency with deploy hosts
| Risk | Mitigation |
|---|---|
| Image modules diverge from live host | Share roles/* and profiles/* |
| Disko vs image partitioning | Images often use generator partitioning; Disko for bare metal |
| Secrets in images | Never bake production private keys into public artifacts |
| Huge closure | Slim profiles for images |
Different stateVersion |
One policy; document exceptions |
Theory 5 — Alternatives awareness
| Tool | Notes |
|---|---|
nixos-generators |
Friendly multi-format entry point |
make-disk-image internals |
Under many formats; lower-level |
nixos-rebuild build-vm |
Quick smoke VM, not always production image |
| Packer + Nix | Cross-cloud pipelines |
| Discovered AMI publish | Separate CI concern (Day 66+) |
nixos-generators + upload scripts |
Your glue, not the generator’s job |
Theory 6 — Size, caches, and CI
Image builds are full system closures. First build is slow; second should hit substitutes.
| Lever | Effect |
|---|---|
| Binary cache with your system paths | Huge wall-clock win |
documentation.*.enable = false |
Noticeable MB |
| Drop unused locales / firmware | Context-dependent |
| Separate “lab fat” vs “edge slim” profiles | Avoid one profile for all |
| CI artifact store | Keep last N images; GC ruthlessly |
Record image size in notes every rebuild during the lab—train the instinct that modules have mass.
Theory 7 — Security of publishable artifacts
Anything you upload to object storage is world-readable unless you lock it down.
# before publish: secret hygiene on image module path
rg -n "PRIVATE|AGE-SECRET|BEGIN OPENSSH PRIVATE|sk-" hosts modules || true| OK to bake | Never bake |
|---|---|
| Lab public SSH keys | Age/sops private keys |
| Hardened sshd defaults | Cloud API tokens |
| Public CA certs | Production host private keys |
| Non-secret config | networking.wireless PSKs |
Production: inject secrets at first deploy (Day 34–35 patterns) or via cloud metadata your threat model accepts.
Worked example — boot qcow with QEMU
nix build .#edge-qcow -L
IMG=$(readlink -f result)
# if result is a dir, find the qcow2:
find result -name '*.qcow2' -o -name '*.img'
qemu-system-x86_64 \
-enable-kvm \
-m 2048 \
-drive file="$IMG",if=virtio \
-nic user,hostfwd=tcp::2222-:22 \
-nographic
# ssh -p 2222 root@127.0.0.1Adjust for copy-on-write overlay so you do not dirty the store image:
qemu-img create -f qcow2 -b "$IMG" -F qcow2 /tmp/edge-overlay.qcow2
qemu-system-x86_64 \
-enable-kvm -m 2048 \
-drive file=/tmp/edge-overlay.qcow2,if=virtio \
-nic user,hostfwd=tcp::2222-:22 \
-nographicLab — multi-step
Suggested notes: ~/lab/90daysofx/02-nixos/day62
Step 1 — Add generator input
Pin; follows nixpkgs; commit flake.lock.
Step 2 — Define slim image module
Reuse baseline; bake lab SSH key only. Set system.stateVersion = "26.05".
Step 3 — Build one format
Prefer qcow/iso based on what you can boot easily.
nix build .#edge-qcow -L
du -h $(readlink -f result)Step 4 — Boot once
QEMU, VirtualBox, or cloud upload. Capture login proof (SSH or console) in notes.
Step 5 — Compare modules
Table: image module vs live hosts/edge—shared vs divergent. Fix any accidental fork.
Step 6 — Size pass
Disable docs; remove heavy packages; rebuild; note delta in MB and wall time.
Step 7 — Secret hygiene check
rg on image path—must be clean for publishable images. Record command + empty result.
Step 8 — Optional second format
ISO installer with same baseline for recovery story (feeds Day 88 thinking).
Step 9 — Write IMAGE.md
| Field | Value |
|---|---|
| Format | |
| Flake attr | |
| Modules shared | |
| Image-only modules | |
| Size | |
| Boot proof | path to log |
| Publish? | yes/no + why |
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Wrong format name | Check generator docs for pin |
| Cannot SSH | keys, firewall, grow services, serial console |
| Image boots black screen | serial console -nographic; qemu-guest modules |
| Mutating store image | Use overlay qcow |
| 20GB “minimal” | Slim profile; no chrome; no full texlive |
| Secrets baked | Rebuild without; use cloud metadata / sops on first deploy |
| Generators nixpkgs drift | inputs.nixos-generators.inputs.nixpkgs.follows |
| Arch mismatch (build aarch on x86) | Cross or native builder (Day 54) |
| ISO vs installed system confusion | ISO boots live env; install path still needs nixos-install story |
Checkpoint
- Generator produces an artifact
- Artifact boots
- SSH or console access works
- Shared modules reused (table in
IMAGE.md)
- Secret hygiene checked
- Size recorded before/after slim pass
Commit
git add .
git commit -m "day62: nixos-generators image boot proof"Write three personal gotchas before continuing.
Tomorrow
Day 63 — Terraform boundary: TF provisions, Nix configures (sketch is enough).