Day 62 — Images & nixos-generators

Updated

July 30, 2026

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.

Note

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 it

follows 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

Shared vs image-only modules

modules/profiles/baseline.nix   ← both image + live
modules/roles/edge.nix          ← both
hosts/edge/hardware.nix         ← live only (Disko, UUID)
hosts/edge/image.nix            ← image only (guest, keys, slim)

If baseline grows a desktop stack “because laptop,” your AMI explodes. Keep baseline honest.


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.1

Adjust 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 \
  -nographic

Proof of shared modules

# after SSH in
cat /etc/os-release
nixos-version
# if you bake a marker:
# cat /etc/day62-marker

Lab — 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).