Configuring Your First NixOS Machine

Updated

September 12, 2026

Configuring Your First NixOS Machine

The installer left you with /etc/nixos/configuration.nix and a generation 1. The boring default from here is: move that config into a flake in git, keep hardware-configuration.nix generated, and treat nixos-rebuild switch --flake as the only mutation path.

The Installing NixOS chapter got a VM to boot. This chapter is how that VM becomes a host you can clone.

Mental model

File Who writes it How often it changes
hardware-configuration.nix nixos-generate-config When disks or initrd modules change
configuration.nix / host module You Every service, user, package
flake.nix + flake.lock You Pin nixpkgs; add the host output
/run/current-system nixos-rebuild Every successful switch

Rebuild verbs:

Command Activates now? Bootloader default? Operational use case
switch Yes Yes Confirmed changes ready for daily use
test Yes No Firewall, network, or SSH tweaks (reboot recovers state)
boot No Yes Kernel upgrades, sysctls, or maintenance during active hours
dry-activate No No Inspect systemd unit restart diffs before applying
build No No Syntax and derivation build check (writes ./result)
git flake  --nixos-rebuild switch-->  new generation
                                      ├── /nix/store/…-nixos-system-…
                                      ├── bootloader entry
                                      └── /run/current-system

system.stateVersion stays at the birth release. It is not “which channel I follow.”

Worked examples

Case 1: A readable host module

Save as configuration.nix (or hosts/desk-vm.nix in a flake):

# configuration.nix
{ config, pkgs, ... }:

{
  imports = [ ./hardware-configuration.nix ];

  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;

  networking.hostName = "desk-workstation";
  time.timeZone = "UTC";
  i18n.defaultLocale = "en_US.UTF-8";

  environment.systemPackages = with pkgs; [
    git
    ripgrep
    htop
    curl
  ];

  services.openssh.enable = true;

  system.stateVersion = "26.05";
}

Leave hardware-configuration.nix as generated. Do not pretty-print UUIDs into /dev/vda2.

Case 2: Flake output for the same host

Save as flake.nix next to those files:

# flake.nix
{
  description = "Desk workstation";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

  outputs = { self, nixpkgs }: {
    nixosConfigurations.desk-workstation = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [ ./configuration.nix ];
    };
  };
}

The output name should match networking.hostName (or be a short alias you document). Rebuild:

sudo nixos-rebuild switch --flake .#desk-workstation

Output:

building the system configuration...
updating systemd-boot...
activating the configuration...
setting up /etc...

--flake .#desk-workstation reads flake.lock. Two machines with the same lock build the same closure (hardware module aside).

Case 3: test before switch

Add cowsay to environment.systemPackages, then:

sudo nixos-rebuild test --flake .#desk-workstation
cowsay desk

Output:

 ____
< desk >
 ----

Reboot. cowsay is gone: test did not update the bootloader default. That is the point — try a service, reboot if it bricks the session, land on the last switch.

When you like it:

sudo nixos-rebuild switch --flake .#desk-workstation

Case 4: Dry-activate, generations, and bootloader recovery

sudo nixos-rebuild dry-activate --flake .#desk-workstation
nixos-rebuild list-generations

Output (shape):

would restart the following units: …
would start the following units: …

Generation  Build-date           NixOS version          Configuration
  1         2026-09-01 10:00:00  26.05.9440.6aefcda     
  2         2026-09-09 12:00:00  26.05.9440.6aefcda     (current)

dry-activate is how you see that an nginx change restarts nginx and does not restart sshd. Read it before switch on a machine people are using.

Rollback in a running shell:

sudo nixos-rebuild switch --rollback

If a bad change breaks networking or panics early boot before you get a shell, do not hunt for a live USB: reboot the physical or virtual host and select Generation 1 in the systemd-boot (or GRUB) menu. NixOS stores previous closures until you run garbage collection.

Case 5: Scaling to a multi-node homelab (hosts/ and modules/)

When you add a second machine (e.g. homelab-docker), do not clone a giant monolithic configuration.nix. Split your repository into host identities and reusable role modules:

.
├── flake.nix
├── flake.lock
├── hosts/
│   ├── desk-workstation/
│   │   ├── default.nix
│   │   └── hardware-configuration.nix
│   └── homelab-docker/
│       ├── default.nix
│       └── hardware-configuration.nix
└── modules/
    ├── common.nix
    └── docker-host.nix

Save baseline settings in modules/common.nix:

# modules/common.nix
{ pkgs, ... }:

{
  nix.settings.experimental-features = [ "nix-command" "flakes" ];

  time.timeZone = "UTC";
  i18n.defaultLocale = "en_US.UTF-8";

  environment.systemPackages = with pkgs; [
    git
    ripgrep
    htop
    curl
    tmux
  ];

  services.openssh = {
    enable = true;
    settings.PasswordAuthentication = false;
    settings.KbdInteractiveAuthentication = false;
  };
}

Save reusable server roles in modules/docker-host.nix:

# modules/docker-host.nix
{ pkgs, ... }:

{
  virtualisation.docker = {
    enable = true;
    autoPrune = {
      enable = true;
      dates = "weekly";
    };
  };

  environment.systemPackages = with pkgs; [
    docker-compose
  ];
}

Compose both machines in flake.nix:

# flake.nix
{
  description = "Homelab infrastructure";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

  outputs = { self, nixpkgs }: {
    nixosConfigurations = {
      desk-workstation = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./hosts/desk-workstation
          ./modules/common.nix
        ];
      };

      homelab-docker = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./hosts/homelab-docker
          ./modules/common.nix
          ./modules/docker-host.nix
        ];
      };
    };
  };
}

Rebuilding any specific node in the fleet is just pointing to its target attr:

sudo nixos-rebuild switch --flake .#homelab-docker

Case 6: Declarative package hygiene

In conventional Linux distributions, machines accumulate hundreds of forgotten packages via apt install or dnf install. On NixOS, the equivalent mistake is turning environment.systemPackages into a dumping ground.

Maintain strict three-tier package hygiene:

  1. Host-wide baseline (modules/common.nix): Only tools required on every node for triage and administration (git, curl, htop, tmux, ripgrep).
  2. Role packages (modules/docker-host.nix, modules/db.nix): Packages tied directly to that role’s operation (e.g. docker-compose or postgresql_17).
  3. Ad-hoc exploratory tools (no system rebuild): For one-off diagnostics, benchmarks, or utilities (iperf3, nmap, fastfetch), do not edit your configuration. Run them ephemerally:
nix run nixpkgs#iperf3 -- -s
nix shell nixpkgs#nmap -c nmap 192.168.1.0/24

These download and run directly from the Nix store without polluting the host closure or creating a new system generation.

The trap

The trap is changing system.stateVersion when you bump nixpkgs to 26.11. Databases, stateVersion-gated defaults, and Home Manager state dirs then migrate in surprising ways. Follow the release notes. Keep the birth value until a note tells you to bump a specific option.

Another trap is running nixos-rebuild switch over SSH when modifying firewall rules, network interfaces, or SSH daemon options. If a rule syntax or port binding fails, you are severed from the machine. Always run nixos-rebuild test for remote connectivity changes: a hard reboot or hypervisor reset will immediately restore the previous generation.

A third trap: editing /etc/nixos on the live machine and never pushing git. The next nixos-anywhere or the next laptop rebuilds last week’s host. The flake in git is the machine. /etc/nixos is either a checkout or a leftover from the installer — pick one.

A fourth trap: nixos-rebuild switch without --flake after you moved to a flake. You just built the old /etc/nixos/configuration.nix against channels. nixos-version and nix flake metadata should agree.

boot.loader.systemd-boot.configurationLimit = 8; so /boot does not fill. nix.gc.automatic with --delete-older-than 14d so the store does not. Neither is a substitute for git being the machine.

The boring rule

  • Flake in git. --flake .#host. Lockfile committed.
  • hardware-configuration.nix generated; host module written by you.
  • test for network/firewall/SSH changes; boot for kernel updates during working hours; switch when confirmed; dry-activate before production restarts.
  • Organize fleets with hosts/<name>/default.nix and shared modules/. No framework needed.
  • Keep systemPackages lean. Use nix run or nix shell for ad-hoc exploration.
  • ESP configurationLimit + 14-day GC. Still --flake .#host.
  • stateVersion is a birth certificate.

Try this

  1. nixos-version and nix flake metadata — confirm the nixpkgs revision in the lock matches what you think is running.
  2. nixos-rebuild list-generations to inspect your current generation history and timestamps.
  3. sudo nixos-rebuild test with a hostname change, hostname, reboot, hostname again. Then switch if you want it permanent.
  4. Run nix run nixpkgs#fastfetch directly without adding it to environment.systemPackages. Verify that which fastfetch reports nothing after the command exits.
  5. sudo nixos-rebuild dry-activate after adding services.nginx.enable = true; (lab VM). Read which units would start. Disable nginx if you do not need it.