Configuring Your First NixOS Machine
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-workstationOutput:
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 deskOutput:
____
< 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-workstationCase 4: Dry-activate, generations, and bootloader recovery
sudo nixos-rebuild dry-activate --flake .#desk-workstation
nixos-rebuild list-generationsOutput (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 --rollbackIf 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-dockerCase 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:
- Host-wide baseline (
modules/common.nix): Only tools required on every node for triage and administration (git,curl,htop,tmux,ripgrep). - Role packages (
modules/docker-host.nix,modules/db.nix): Packages tied directly to that role’s operation (e.g.docker-composeorpostgresql_17). - 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/24These 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.nixgenerated; host module written by you.testfor network/firewall/SSH changes;bootfor kernel updates during working hours;switchwhen confirmed;dry-activatebefore production restarts.- Organize fleets with
hosts/<name>/default.nixand sharedmodules/. No framework needed. - Keep
systemPackageslean. Usenix runornix shellfor ad-hoc exploration. - ESP
configurationLimit+ 14-day GC. Still--flake .#host. stateVersionis a birth certificate.
Try this
nixos-versionandnix flake metadata— confirm the nixpkgs revision in the lock matches what you think is running.nixos-rebuild list-generationsto inspect your current generation history and timestamps.sudo nixos-rebuild testwith a hostname change,hostname, reboot,hostnameagain. Thenswitchif you want it permanent.- Run
nix run nixpkgs#fastfetchdirectly without adding it toenvironment.systemPackages. Verify thatwhich fastfetchreports nothing after the command exits. sudo nixos-rebuild dry-activateafter addingservices.nginx.enable = true;(lab VM). Read which units would start. Disable nginx if you do not need it.