Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Write a first NixOS configuration

By updated

Improve this page

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.nix section 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.org and nixos-option
  • Write a concrete configuration: packages, hardened SSH, admin user
  • Apply the changes with nixos-rebuild switch without 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:

PartRole
{ 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
}
SectionWhat it controlsImportant remark
imports.nix files included in the evaluationhardware-configuration.nix is generated by nixos-generate-config
boot.loaderGRUB or systemd-boot, BIOS or UEFIDepends on the boot mode of your machine
networking.*Host name, interfaces, DNS, firewallnetworkmanager for workstations, firewall for servers
time.timeZoneSystem time zoneIANA format: Europe/Paris, UTC, America/New_York
environment.systemPackagesPackages available to every userwith pkgs; brings the nixpkgs scope into the list
services.*Enabling and configuring servicesOne NixOS module per service
users.users.*Declaring users and groupsEach user is an attrset of its own configuration
system.stateVersionNixOS version at the time of the initial installationNever 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 isA piece of software to make availableA declaration of system behaviour
Where it goesIn environment.systemPackagesDirectly in the body of the config
What it doesMakes the binary reachable in the PATHInstalls the package and configures and starts the service
Examplepkgs.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 everything
services.nginx.enable = true;
# INCORRECT: Redundant and a source of conflicts
environment.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=openssh

2. nixos-option, queries the options available on the running system:

Terminal window
# List the sub-options of a service
nixos-option services.openssh
# Get the current value + description of an option
nixos-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:

Terminal window
nix repl --file '<nixpkgs/nixos>'
# nix-repl> :t options.services.openssh
# nix-repl> options.services.openssh.enable

First 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, htop available on the command line
  • SSH enabled with password authentication disabled
  • An admin user in the wheel group (sudo access)
  • Europe/Paris time 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.

  1. Edit the file (as root or through sudo)

    Terminal window
    sudo vim /etc/nixos/configuration.nix
  2. 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
  3. Apply the changes

    Terminal window
    sudo nixos-rebuild switch
    # Build + immediate activation + default bootloader entry
  4. 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.

MistakeSymptomSolution
Forgetting the ;error: syntax error, unexpected …, expecting ';'Each option in an attrset ends with ;
Mixing up [ ] and { }Type error during evaluationsystemPackages expects a list [ ], not an attrset { }
Putting a package where an option is expectedA 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 Gitgit add the new files before nixos-rebuild; without a flake, Git plays no part
Changing system.stateVersionUnpredictable behaviour on existing dataNever 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 in pkgs nor anywhere else: probably a typo. Written as pkgs.gittt, the same slip gives attribute 'gittt' missing.
  • at …:12:5, line 12, column 5, the cursor is precise.
  • Fix: nix search nixpkgs git to 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 configuration

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

ErrorLikely causeCheckSolution
error: syntax errorMissing ; or unclosed braceLine given in the error messageCheck the ; at the end of each option, balanced { }
undefined variable 'xxx' or attribute 'xxx' missingNon-existent package or optionnix search nixpkgs xxx or nixos-option xxxFix the name or find the equivalent option
nixos-rebuild switch seems stuckLarge dependency buildRun again with --print-build-logs (-L) to see the build logsWait, the first activation can be long
Failed to start sshd.serviceInvalid SSH configuration or port already in usesystemctl status sshd + journalctl -u sshdCheck services.openssh.settings, port 22 free
Service missing after the rebuildOption enable = false or not declarednixos-option services.xxx.enableAdd or fix the option in the config
error: path '/etc/nixos/hardware-configuration.nix' does not existFile missing from the imported pathls /etc/nixos/Regenerate it with sudo nixos-generate-config

Key points

  • configuration.nix is a Nix function, it receives config and pkgs, 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-option locally to find the right option.
  • nixos-rebuild dry-build validates without building or applying anything, use it systematically before switch.
  • system.stateVersion is 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

6 questions
6 min
70% to pass

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

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn