Images with nixos-generators

Updated

July 30, 2026

Images with nixos-generators

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, 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 book.

Why it matters

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 — nixos-generators integration shapes

Flake app / package patterns

Community patterns include:

# Conceptual — verify against current nixos-generators docs
inputs.nixos-generators.url = "github:nix-community/nixos-generators";
inputs.nixos-generators.inputs.nixpkgs.follows = "nixpkgs";
nix run github:nix-community/nixos-generators -- --help
# format flags and flake integration evolve

Module specializations

Images often need:

Concern Image-friendly approach
Hostname Generic or cloud-init later
SSH keys Inject via cloud-init / metadata; or bake lab keys carefully
Disko May differ from bare-metal Disko; know format’s disk story
Size Minimal profiles; avoid workstation GUIs on cloud images
First boot growpart, regenerate machine-id policies

Theory 3 — Same modules, different entrypoints

# modules shared
imports = [
  ../modules/profiles/baseline.nix
  ../modules/roles/web.nix
];

# hosts/edge — deploy target
# images/edge — generators entry with format-specific tweaks only
Allowed in image entry Forbidden without reason
Format modules Entirely different nginx config
Cloud agent packages Hardcoded prod secrets
Temporary serial console Divergent firewall policy forever

If image and live host diverge by 200 lines, you have two products.


Theory 4 — Cloud-init and metadata boundary

Terraform/cloud metadata often supplies:

  • SSH keys
  • Hostname
  • Network

NixOS modules should consume those mechanisms or disable conflicting agents intentionally.

Smell Fix
500-line bash user-data Bake image; tiny user-data
Image with prod secrets Inject at instance start via sops/metadata carefully
No SSH to new instance Console + known image user/key story

Theory 5 — Build performance and caches

Image builds are heavy. Use:

  • Binary caches for the system closure
  • Remote builders with disk
  • CI only on main or nightly for full images; PR builds eval/smaller checks
nix build .#images.edge-qcow -L
ls -lh result*

Theory 6 — ISO as recovery

A flake-built installer ISO that includes your SSH key and network helpers is a recovery product:

laptop dies → boot ISO → clone flake → Disko → install

Pair with Disko chapter reinstall proofs. Store ISO hashes in runbooks if you distribute them.


Worked example — qcow lab image (shape)

# Illustrative structure — adapt to current generators API
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
  # inputs.nixos-generators = …;

  outputs = { self, nixpkgs, ... }: {
    nixosConfigurations.edge-image = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./modules/profiles/baseline.nix
        ./modules/roles/web.nix
        ./images/edge-image.nix
      ];
    };
    # packages.x86_64-linux.edge-qcow = … generators expression …
  };
}
# images/edge-image.nix
{ lib, modulesPath, ... }:
{
  imports = [
    (modulesPath + "/profiles/qemu-guest.nix")
  ];
  services.openssh.enable = true;
  # lab only:
  users.users.root.openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAA… lab" ];
  system.stateVersion = "26.05";
}

Boot under qemu:

qemu-system-x86_64 -m 2048 -enable-kvm -drive file=./result/nixos.qcow2,if=virtio
# paths vary by generator output layout

Prove:

ssh root@… cat /etc/os-release
# compare packages/options to live edge host where possible

Worked example — consistency checklist

# IMAGE-CONSISTENCY.md
- Module list shared with hosts/edge: yes/no
- Divergences (intentional): …
- Format: qcow2 / iso / …
- Build command:
- Boot proof date:
- SSH method:

Lab — multi-step

Suggested notes: ~/lab/nixos/ops/images

Step 1 — Pick a format

qcow2 for local hypervisor is the friendliest lab path. ISO if you care about recovery.

Step 2 — Wire generators input

follows nixpkgs; read current README for flake examples.

Step 3 — Image module entry

Import the same baseline/role as a live host. Only format-specific imports extra.

Step 4 — Build artifact

# per current docs
nix build … -L
du -h result*

Step 5 — Boot once

QEMU/libvirt/VirtualBox—whatever you have. Reach SSH or console.

Step 6 — Consistency proof

Compare nixos-version, hostname policy, and one role-provided package/service to the live host story.

Step 7 — Size diet (optional)

Remove fat packages; rebuild; note image size delta.

Step 8 — Document CI policy

When do you rebuild images? Who may publish them?

Step 9 — Stretch cloud

If credentials exist, note AMI/GCE path as stretch—do not block on cloud.


Exercises

  1. List five things that must never be baked into a published image.
  2. Design a versioning scheme for golden images (flake rev + date).
  3. Explain why Disko for metal and cloud image root disk may differ.
  4. Sketch a pipeline: CI builds qcow → uploads → TF launches.
  5. Recovery ISO contents checklist (network, git, ssh, disko).

Common gotchas

Symptom / mistake What to do
Format name changed Read current generators docs
Image huge Minimal profile; multi-out not enough—fewer packages
Cannot SSH Console; authorizedKeys; cloud-init race
Divergent modules Re-unify with live host
Build OOM/disk Builder sizing; GC
Secrets in image Remove; inject later
Slow every PR Cache; nightly images
Wrong system arch Match hypervisor/cloud

Checkpoint

  • Generators (or equivalent) wired
  • Image built from shared modules
  • Booted once with proof
  • Consistency notes vs live host
  • CI/publish policy sketched
  • No secrets baked intentionally

Commit

git add .
git commit -m "ops: nixos-generators image from shared modules"

Write three personal gotchas before Terraform boundary.