A configuration.nix file is a Nix expression that declares the complete state of a NixOS system. You do not run commands to install packages or enable services, you describe what you want, and nixos-rebuild switch works out exactly what needs doing. Mastering configuration.nix means mastering the layer that turns the Nix language (attrsets, functions, options) into a living system. This guide shows how to make the jump from "I know attrsets" to "I have a working system configuration". The behaviours of the module system described here were checked by evaluation with Nix 2.35.2 and the nixpkgs of 29 September 2026; what requires a real NixOS machine (nixos-rebuild, nixos-option) follows the NixOS 26.05 manual.
What you will learn
- Read the general structure of a Nix file: expression,
{ config, pkgs, ... }:arguments, output block - Understand the anatomy of a minimal
configuration.nixsection by section - Tell a package (
pkgs.xxx) from a NixOS option (services.xxx.enable) - Find the right option in the NixOS documentation with
search.nixos.organdnixos-option - Write a concrete configuration: packages, hardened SSH, admin user
- Apply the changes with
nixos-rebuild switchwithout rebooting the machine - Modularise into separate files with
imports
In what context?
As soon as you work on a NixOS system, in a test VM, on a production server or in a homelab, configuration.nix is the central file. Knowing how to read and modify it becomes essential to:
- customise a NixOS server installed from an image or a cloud template,
- reproduce an identical environment on several machines from a Git repository,
- enable a service (SSH, Docker, Nginx) without looking for the right installation command,
- diagnose why a service does not start after a rebuild,
- prepare the move to flakes by first understanding the basic configuration.
This guide builds on the Nix language basics (attrsets, functions, let, with). If these notions are not familiar yet, start with that guide.
The general form of a Nix file
Every .nix file is an expression, a computed value, not a script. A configuration.nix is a function that receives arguments and returns an attrset of options:
{ config, pkgs, ... }:{ # your options here}The three essential parts:
| Part | Role |
|---|---|
{ config, pkgs, ... }: | Arguments that NixOS injects at evaluation time. config exposes the values of the whole configuration; pkgs gives access to nixpkgs. ... accepts other arguments without naming them. |
{ ... } | The body of the function: an attrset in which each attribute is a NixOS option (networking.hostName, services.openssh.enable, etc.). |
; | Each option in the attrset ends with a semicolon, it is Nix syntax, not YAML. |
Anatomy of a minimal configuration.nix
Here is the complete skeleton of a basic configuration, commented section by section:
{ config, pkgs, ... }:
{ # ── 1. Imports ────────────────────────────────────────────────────────────── imports = [ ./hardware-configuration.nix # generated automatically during installation ];
# ── 2. Bootloader ─────────────────────────────────────────────────────────── boot.loader.systemd-boot.enable = true; boot.loader.efi.canTouchEfiVariables = true;
# ── 3. Network ────────────────────────────────────────────────────────────── networking.hostName = "monserveur"; networking.networkmanager.enable = true;
# ── 4. Locale and time zone ───────────────────────────────────────────────── time.timeZone = "Europe/Paris"; i18n.defaultLocale = "fr_FR.UTF-8";
# ── 5. System packages ────────────────────────────────────────────────────── environment.systemPackages = with pkgs; [ git vim curl wget ];
# ── 6. Services ───────────────────────────────────────────────────────────── services.openssh.enable = true;
# ── 7. Users ──────────────────────────────────────────────────────────────── users.users.admin = { isNormalUser = true; extraGroups = [ "wheel" ]; };
# ── 8. State version ──────────────────────────────────────────────────────── system.stateVersion = "26.05"; # ← never change it after installation}| Section | What it controls | Important remark |
|---|---|---|
imports | .nix files included in the evaluation | hardware-configuration.nix is generated by nixos-generate-config |
boot.loader | GRUB or systemd-boot, BIOS or UEFI | Depends on the boot mode of your machine |
networking.* | Host name, interfaces, DNS, firewall | networkmanager for workstations, firewall for servers |
time.timeZone | System time zone | IANA format: Europe/Paris, UTC, America/New_York |
environment.systemPackages | Packages available to every user | with pkgs; brings the nixpkgs scope into the list |
services.* | Enabling and configuring services | One NixOS module per service |
users.users.* | Declaring users and groups | Each user is an attrset of its own configuration |
system.stateVersion | NixOS version at the time of the initial installation | Never change it, it manages the compatibility of persistent data |
In this table, the Important remark column is the one that avoids bad surprises. Two rows deserve a careful reading: imports, because hardware-configuration.nix describes the disks and hardware detected at installation time and a missing file blocks the rebuild; and system.stateVersion, which freezes the behaviour of stateful services (PostgreSQL database format, data location) and therefore does not follow the installed NixOS version.
Packages vs options: the key distinction
This is the most frequent source of confusion for new NixOS users: a package makes a binary available, an option describes a behaviour of the system.
Package (pkgs.xxx) | Service option (services.xxx.enable) | |
|---|---|---|
| What it is | A piece of software to make available | A declaration of system behaviour |
| Where it goes | In environment.systemPackages | Directly in the body of the config |
| What it does | Makes the binary reachable in the PATH | Installs the package and configures and starts the service |
| Example | pkgs.nginx (just the binary) | services.nginx.enable = true; (the whole thing) |
Practical rule: if a service has a NixOS module (services.xxx), use it, it manages the packages, the configuration and the systemd unit. Do not add nginx to environment.systemPackages if you use services.nginx.enable = true;.
# CORRECT: the NixOS module handles everythingservices.nginx.enable = true;
# INCORRECT: Redundant and a source of conflictsenvironment.systemPackages = with pkgs; [ nginx ];services.nginx.enable = true;Find the right option
The exact name of an option cannot be guessed: services.openssh.settings.PasswordAuthentication looks nothing like the PasswordAuthentication no of an sshd_config. Three tools answer the question, from the fastest to the most precise. The web search engine is for exploring a domain you do not know; nixos-option answers on your machine, with the value actually applied; nix repl is for when you want to inspect the structure of a module before writing anything.
1. search.nixos.org, the official search engine, the fastest:
https://search.nixos.org/options?query=openssh2. nixos-option, queries the options available on the running system:
# List the sub-options of a servicenixos-option services.openssh
# Get the current value + description of an optionnixos-option services.openssh.enable# Value: true# Default: false# Description: Whether to enable the OpenSSH secure shell daemon, which allows secure remote logins.# Declared by: <nixpkgs/nixos/modules/services/networking/ssh/sshd.nix>3. nix repl with nixpkgs loaded:
nix repl --file '<nixpkgs/nixos>'# nix-repl> :t options.services.openssh# nix-repl> options.services.openssh.enableFirst hands-on case
Goal
Configure a NixOS server with the four elements below, each carried by a precise option that the complete configuration makes visible:
git,vim,curl,htopavailable on the command line- SSH enabled with password authentication disabled
- An
adminuser in thewheelgroup (sudo access) Europe/Paristime zone
The complete configuration
Three blocks of this file carry the real decisions. The services.openssh.settings block turns off password authentication and root login: if you apply this configuration without having deployed your public key beforehand, you lose remote access to the machine. The users.users.admin block contains the expected public key, to replace with yours. Finally, security.sudo.wheelNeedsPassword = false; grants passwordless sudo to the wheel group: handy in a lab, to remove on an exposed machine where the compromise of an SSH session would hand over root directly.
{ config, pkgs, ... }:
{ imports = [ ./hardware-configuration.nix ];
boot.loader.systemd-boot.enable = true; boot.loader.efi.canTouchEfiVariables = true;
networking.hostName = "serveur-prod"; networking.networkmanager.enable = true;
time.timeZone = "Europe/Paris"; i18n.defaultLocale = "fr_FR.UTF-8";
environment.systemPackages = with pkgs; [ git vim curl htop ];
services.openssh = { enable = true; settings = { PasswordAuthentication = false; PermitRootLogin = "no"; }; };
users.users.admin = { isNormalUser = true; description = "Administrateur système"; extraGroups = [ "wheel" ]; openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... votre_cle_publique" ]; };
security.sudo.wheelNeedsPassword = false;
system.stateVersion = "26.05";}Apply the changes
The order of the four steps is not decorative. dry-build evaluates the Nix expression and shows what would be built or downloaded, without building anything or touching the system: a forgotten brace or a non-existent package fails here, not after SSH has been disabled. The verification step is the only one that proves the configuration had the expected effect: a nixos-rebuild switch that ends without an error means the system was activated, not that the service is running.
-
Edit the file (as root or through sudo)
Terminal window sudo vim /etc/nixos/configuration.nix -
Validate the configuration without applying it
Terminal window sudo nixos-rebuild dry-build# Evaluate and list what would be built: error output if the syntax or an option is wrong -
Apply the changes
Terminal window sudo nixos-rebuild switch# Build + immediate activation + default bootloader entry -
Check the result
Terminal window # Packages available?which git vim curl htop# SSH started?systemctl status sshd# User created?id admin
Classic mistakes and how to avoid them
The five mistakes below account for most of the blockers met during the first nixos-rebuild runs. Read the Symptom column first: it matches what you have in front of you. The first four rows fail at evaluation, with an explicit message, and therefore break nothing. The last one is sneakier: the rebuild succeeds, but the result is not the expected one.
| Mistake | Symptom | Solution |
|---|---|---|
Forgetting the ; | error: syntax error, unexpected …, expecting ';' | Each option in an attrset ends with ; |
Mixing up [ ] and { } | Type error during evaluation | systemPackages expects a list [ ], not an attrset { } |
| Putting a package where an option is expected | A definition for option `services.nginx.enable' is not of type `boolean' | services.nginx.enable = pkgs.nginx is wrong, write = true |
Forgetting git add before the rebuild (flake configuration) | Path '…' in the repository … is not tracked by Git | git add the new files before nixos-rebuild; without a flake, Git plays no part |
Changing system.stateVersion | Unpredictable behaviour on existing data | Never change it after the initial installation |
Read a Nix error message
A Nix error reads from top to bottom: the first line says what failed, the following ones say where. The evaluator points directly at the faulty fragment with a ^ cursor, and --show-trace unfolds the stack when the error comes from an imported file. Here is a common case, a typo in a package name written inside a with pkgs; block:
error: undefined variable 'gittt' at /etc/nixos/configuration.nix:12:5: 11| environment.systemPackages = with pkgs; [ 12| gittt | ^ 13| ];undefined variable 'gittt', the name exists neither inpkgsnor anywhere else: probably a typo. Written aspkgs.gittt, the same slip givesattribute 'gittt' missing.at …:12:5, line 12, column 5, the cursor is precise.- Fix:
nix search nixpkgs gitto find the exact name.
First modularisation: splitting the configuration
As soon as configuration.nix goes beyond fifty lines or so, split it into thematic files: each administration domain gets a file you can reread at a glance, and reuse on another machine.
Proposed structure
A useful split follows administration domains, not option types: what you look for when you come back to the machine is "where are the users declared", not "where are the booleans". configuration.nix remains the entry point and only keeps the global options (bootloader, host name, time zone); each imported file is self-contained and can be copied as is to another machine.
/etc/nixos/├── configuration.nix ← main entry + global options├── hardware-configuration.nix├── users.nix ← user declarations└── services/ └── ssh.nix ← SSH configurationusers.nix
Each extracted file keeps the same form as configuration.nix: a function that receives { config, pkgs, ... }: and returns an attrset of options. It is not a style requirement, it is what lets the evaluator merge all the files into a single configuration. Here, only the users domain is declared.
{ config, pkgs, ... }:
{ users.users.admin = { isNormalUser = true; extraGroups = [ "wheel" ]; openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI..." ]; };}services/ssh.nix
The SSH hardening moves to its own file, in a services/ subfolder. The benefit becomes obvious from the second machine: this file is the security contract of your remote access, you reread it in one go instead of hunting for it in the middle of fifty unrelated option lines.
{ config, pkgs, ... }:
{ services.openssh = { enable = true; settings = { PasswordAuthentication = false; PermitRootLogin = "no"; }; };}configuration.nix with imports
The entry file slims down: it only holds the imports list and the options that belong to no particular domain. Paths are relative to the current file (./services/ssh.nix), not to the working directory, and Nix refuses a non-existent path at evaluation time.
{ config, pkgs, ... }:
{ imports = [ ./hardware-configuration.nix ./users.nix ./services/ssh.nix ];
boot.loader.systemd-boot.enable = true; boot.loader.efi.canTouchEfiVariables = true;
networking.hostName = "serveur-prod"; time.timeZone = "Europe/Paris";
environment.systemPackages = with pkgs; [ git vim curl htop ];
system.stateVersion = "26.05";}nixos-rebuild switch evaluates all the files listed in imports as if they formed a single one. In practice, two files that give the same option two different values cause the The option `networking.hostName' has conflicting definition values error at evaluation; two identical values go through silently. List-type options (environment.systemPackages, imports) are the exception: the module system concatenates their definitions, and two files can each add their own packages. This behaviour is enough to cover most single-machine configurations.
Troubleshooting
Read this table through the Check column: it gives the command that decides between several possible causes before you touch the file. The useful reflex on NixOS is to separate two families of failures. The first three rows block evaluation or the build, the system has not moved and going back is immediate. The last three happen after activation: the new profile is in place, and the previous entry stays selectable at boot to return to the state that worked.
| Error | Likely cause | Check | Solution |
|---|---|---|---|
error: syntax error | Missing ; or unclosed brace | Line given in the error message | Check the ; at the end of each option, balanced { } |
undefined variable 'xxx' or attribute 'xxx' missing | Non-existent package or option | nix search nixpkgs xxx or nixos-option xxx | Fix the name or find the equivalent option |
nixos-rebuild switch seems stuck | Large dependency build | Run again with --print-build-logs (-L) to see the build logs | Wait, the first activation can be long |
Failed to start sshd.service | Invalid SSH configuration or port already in use | systemctl status sshd + journalctl -u sshd | Check services.openssh.settings, port 22 free |
| Service missing after the rebuild | Option enable = false or not declared | nixos-option services.xxx.enable | Add or fix the option in the config |
error: path '/etc/nixos/hardware-configuration.nix' does not exist | File missing from the imported path | ls /etc/nixos/ | Regenerate it with sudo nixos-generate-config |
Key points
configuration.nixis a Nix function, it receivesconfigandpkgs, and returns an attrset of NixOS options.- Package (
environment.systemPackages) and service option (services.xxx.enable) are two distinct mechanisms, NixOS modules do both at once, do not duplicate. - Use search.nixos.org/options or
nixos-optionlocally to find the right option. nixos-rebuild dry-buildvalidates without building or applying anything, use it systematically beforeswitch.system.stateVersionis never changed after the initial installation.- Modularising with
imports = [ ./file.nix ]is simple and effective, split early.
Knowledge check
Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Create reproducible development environments with Nix
A new full quiz with random questions
Work again only on the questions you missed