Nix : Maîtrisez les environnements reproductibles
Comprendre les concepts fondamentaux de Nix et être capable de l'utiliser pour gérer des environnements de développement, packager une application simple.
Introduction
Si vous êtes un développeur ou un administrateur système vous avez sans doute rencontré ces situations :
Le code récupéré
Vous récupérez le code d'un collègue pour l'utiliser sur votre machine.
Celui-ci vous avertit, le code est encore en cours d'écriture mais cela fonctionne sur ma machine.
Après un git clone et une lecture rapide rapide du README.md vous vous lancez dans l'installation.
Mais les ennuis commencent, vous utilisez Debian et votre collègue RedHat, les commandes et les noms des paquets ne sont pas les mêmes.
Ensuite la version d'openssl sur Debian est trop ancienne.
Après 1h sur StackOverflow vous arrivez à installer la version sans casser tout le système.
Maintenant cette librairie étrange.
Elle n'est pas fournie dans Debian, il va falloir la compiler.
Malheureusement vous n'avez pas installé de compilateur.
Après une après-midi vous arrivez enfin à lancer votre application.
L'autre bonne nouvelle est que maintenant le README.md contient une entrée pour l'installation sur une Debian.
L'administrateur système
Notre administrateur système peut être confronté au même problème que le développeur au-dessus, à savoir faire tourner du code sur un serveur.
Mais il a aussi à gérer un parc de serveurs.
Heureusement ils sont tous identiques et provisionnés avec Ansible.
En y regardant de plus près il y a une différence de version sur une librairie entre deux serveurs.
Ah oui un développeur avait besoin d'une version particulière et elle a été installée à la main.
Il faudra faire attention en relançant ansible.
Et ce serveur pourquoi est-ce encore l'ancienne version de ce logiciel. Il semble que la mise à jour de sécurité automatique n'a pas fonctionné.
Comment Nix peut nous aider
On pourrait donner encore d'autres exemples (les images docker qui changent...) mais ces deux exemples suffisent.
Nos outils opèrent sur un état global et les installations peuvent avoir des effets de bord. Les installations ne sont pas reproductibles et sont souvent incrémentales.
Nix n'est pas juste un "meilleur apt". C'est une nouvelle fondation qui offre des garanties que les autres outils ne peuvent pas fournir :
- Reproductibilité parfaite : Fini le "ça marche sur ma machine". Si une dépendance est définie, elle sera reconstruite exactement à l'identique, bit par bit, sur votre machine, sur celle de votre collègue et en CI.
- Isolation totale : Installez 5 versions de
Pythonet 10 versions deopensslsans aucun conflit. Chaque projet vit dans sa bulle hermétique, sans jamais affecter le système global ou les autres projets. - Gestion déclarative: Vous ne décrivez plus une série d'instructions ("installe ça, puis ça, puis configure ça"). Vous décrivez l'état final désiré dans un seul fichier. Nix se charge de le matérialiser. La dérive de configuration devient impossible.
- Rollbacks atomiques : Une mise à jour a tout cassé ? Une seule commande vous ramène instantanément à l'état précédent qui fonctionnait. Fini les restaurations de snapshots angoissantes.
Ce tutoriel va vous montrer, étape par étape, comment passer de la gestion du chaos à la maîtrise de votre environnement. Prêt à commencer ?
Premiers pas - Les environnements à la demande
Nous allons commencer à utiliser Nix sur une Debian déjà configurée.
Avant de commencer on va parler du nix store et du FHS, Filesystem Hierarchy Standard.
Le FHS
Le FHS vous devez connaître, c'est ce qui organise le système de fichiers de la plupart des systèmes UNIX (Linux, macOS, etc.).
Il nous dit que les exécutables des utilisateurs se trouvent dans /usr/bin, les bibliothèques partagées dans /usr/lib, et les fichiers de configuration dans /etc.
Quand on installe un logiciel avec apt ou un autre gestionnaire de paquets on va mettre des fichiers dans ou plusieurs de ces répertoires. Et tous les utilisateurs de la machine peuvent y accéder.
C'est un problème si on a besoin de mettre à jour un version de librairie, il faut le faire pour toutes les applications. Et si une application est cassée et n'est pas compatible avec la nouvelle version ce n'est pas simple de revenir en arrière.
Le /nix/store
C'est ici que Nix opère une rupture philosophique radicale.
Au lieu d'installer notre application dans des répertoires partagés, Nix utilise un entrepôt immuable : le répertoire /nix/store.
À quoi ressemble un chemin dans le Nix store :
/nix/store/1h9m1w6rg365005k7f4zvn18f8v923k1-jq-1.6/bin/jq
/nix/store/s2z997f3nq3zm1dp6cf8v283p32is1h7-openssl-3.0.7/lib/libssl.so
Décortiquons cette structure :
- /nix/store/ : La racine de l'entrepôt. Tout ce que Nix gère se trouve ici. Ce répertoire est en lecture seule pour les utilisateurs standards.
- 1h9m1w6rg365005k7f4zvn18f8v923k1 : C'est un hash cryptographique qui n'est pas calculé sur le nom du paquet, mais sur tous ses ingrédients de construction (sa "dérivation").
Cela inclut :
- Le code source.
- Toutes les dépendances (ex: gcc, openssl), qui sont elles-mêmes des chemins du Nix store.
- Le script de build.
- Les flags de compilation.
- L'architecture du système.
- -jq-1.6 : Un nom lisible pour l'humain, pour faciliter l'identification.
Qu'est-ce que cela signifie ?
- Immutabilité : Une fois qu'un paquet est construit et placé dans le store (par exemple,
...-jq-1.6), son contenu ne changera jamais. Si vous voulez une version patchée dejq, Nix créera un nouveau répertoire avec un nouveau hash. L'ancien reste intact. - Coexistence sans conflit : Vous voulez
opensslversion 1.1 et 3.0 sur le même système ? Aucun problème. Elles vivront dans deux répertoires distincts du store, avec deux hash différents. Une application qui a besoin de la v1.1 pointera explicitement vers le chemin/nix/store/hash-de-la-v1.1/...et l'autre vers/nix/store/hash-de-la-v3.0/.... Elles ne peuvent pas entrer en conflit. - Reproductibilité garantie : Si vous et votre collègue construisez le même paquet avec les mêmes ingrédients, vous obtiendrez exactement le même hash et le même chemin dans le
/nix/store. C'est la fin du "ça marche sur ma machine".
Nix ne gère pas un système de fichiers. Il gère une base de données d'objets de construction où chaque objet est adressable par son contenu. C'est ce qui rend possible les environnements isolés, les rollbacks atomiques et la reproductibilité parfaite.
Un shell éphémère
Pour commencer à utilise Nix nous allons créer un shell éphémère. Cet environnement va nous permettre d'utiliser un programme sans l'installer.
Bonjour le monde, style nix
Le passage obligatoire de toute démonstration est le classique "Hello World !".
Nous allons utiliser le bien nommé paquet hello qui fournit la commande... hello.
Vérifions que la commande hello n'est pas disponible sur notre machine
hello
Sans surprise, le système répondra par une erreur, car cette commande n'est pas installée :
hello: command not found
Ceci est notre état de base, le système est propre.
Maintenant la commande nix shell va nous permettre de créer un shell contenant la commande hello.
nix shell nixpkgs#hello
Patientons quelques instants.
La commande nix shell nixpkgs#hello permet d’ouvrir un shell dans lequel le package hello sera disponible.
Et sans installation globale ni modification permanente de votre système.
Le prompt de notre terminal a changé pour signaler que nous sommes dans un nouveau shell (Modification pour la compréhension du tutoriel). Dans ce shell nous relançons la même commande.
hello
Et nous obtenons le résultat
Hello, world!
Nous pouvons vérifier la version du binaire hello avec la commande :
hello --version
Et nous pouvons vérifier que la commande hello provient bien du nix store et n'est pas installé dans un répertoire système classique :
which hello
/nix/store/b1ayn0ln6n8bm2spz441csqc2ss66az3-hello-2.12.2/bin/hello
La commande nix shell
Prenons un moment pour détailler la commande nix shell nixpkgs#hello.
nix shellest la nouvelle interface de la commande nix, utilisée pour lancer un shell où les paquets spécifiés sont disponibles.nixpkgsfait référence au dépôt principal de paquets Nix, souvent la branche unstable par défaut.#hellodemande le paquet dans le dépôt.
L'environnement ouvert est temporaire et reproductible : aucune modification n’est faite au système de base.
Ces environnements sont utilisés pour tester ou utiliser des exécutables sans polluer l'espace utilisateur.
💡On peut faire référence à d'autres dépôts que nixpkgs et la branche instable. Par exemple pour la branche actuellement stable on utilisera github:nixos/nixpkgs/nixos-25.05. Cela correspond à la version de mai 2025 qui est dans la branche nixos-25.05
🛠️ Petit exercice : comment installer une version vieille de 4 ans, disons celle de mai 2021 ?
On peut ouvrir un shell avec plusieurs paquets comme hello et cowsay.
On peut aussi vouloir lancer une commande directement sans ouvrir de shell interactif.
⚠️ Si on veut lancer directement plusieurs commandes il faut passer la commande par un shell :
nix shell nixpkgs#hello nixpkgs#cowsay --command /bin/sh -c "hello | cowsay"
Les scripts éphémères ?
On peut tout à fait lancer des scripts python sans avoir installé python :
cat > mon_script.py <<EOF
#! /usr/bin/env nix
#! nix shell nixpkgs#python3 --command python
print("Bonjour python !")
EOF
chmod +x mon_script.py
./mon_script.py
Un environnement persistent
Nix permet de définir des environnements de développement (ou de test, production...) qui sont persistants, c'est à dire reproductibles et durables. Une des façons de faire, et qui me semble être la façon "moderne" de faire est d'utiliser des flakes.
Les flakes
Les flakes proposent une structure standardisée : chaque projet repose sur un fichier flake.nix placé à la racine.
Ce fichier déclare explicitement les dépendances (inputs) ainsi que les outputs (packages, devShell, configurations NixOS, etc.).
Chaque version est verrouillée dans un fichier flake.lock.
Cela permet la reproductibilité et la traçabilité de l'environnement.
Les dépendances sont figées à des versions précises (commit git, hash de fichier d'archive...) ce qui rend l'environnement persistant et cohérent dans le temps. L'utilisation de l'environnement sera identique pour tous les utilisateurs et toutes les machines, sans modifier l'environnement de l'utilisateur ou sans que celui-ci modifie notre environnement de travail.
Un environnement de développement python
Nous allons commencer par déclarer un environnement de développement, très simple, pour écrire du python.
mkdir -p dev-python
cd dev-python
cat > flake.nix <<EOF
{
description = "Un environnement de développement pour python";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
let
systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
in
# équivalent à flake-utils.lib.eachDefaultSystem
flake-utils.lib.eachSystem systems (system:
let
pkgs = import nixpkgs { inherit system; };
in
{
devShells.default = pkgs.mkShell {
buildInputs = [ pkgs.python3 ];
};
}
);
}
EOF
💡On peut aussi utiliser l'éditeur de texte pour modifier les fichiers.
Pour se placer dans l'environnement on utilise la commande
nix develop
Le prompt de notre shell a changé et nous signale que nous sommes dans un shell nix.
La commande python est maintenant accessible.
On peut créer un fichier mon_script.py pour afficher un hello world.
Cliquer pour voir un exemple de script
#!/usr/bin/env python3
print("Bonjour python !")
Notre script est on ne peut plus simple. Si on veut faire plus de choses on peut avoir besoin de paquets python. Mais pas question de les installer à la main ou avec pip, on va utiliser les paquets distribués dans le nixpkgs.
On va modifier les buildInputs de cette façon :
buildInputs = [
pkgs.python3
(pkgs.python3.withPackages (ps: with ps; [
numpy
pandas
requests
]))
On modifie les outils qui seront disponibles dans le shell pour ajouter 3 paquets, en plus de python3.
withPackages est une fonction qui va retourner la liste des paquets à inclure dans notre environnement.
Nous retrouverons dans le shell l'exécutable python avec les 3 modules installés, comme avec un pip install.
On peut lancer dans le shell :
python -c "import numpy as np; print(np.random.default_rng(seed=42).integers(low=0, high=10, size
=3))"
flake.lock
Si on regarde le contenu de notre répertoire de travail on remarque un fichier flake.lock.
Il contient les versions des entrées de notre flake.
Dans notre cas la version de nixpkgs est verrouillée sur la version 25.05 sur un commit précis.
Si on veut mettre à jour la source de nos paquets (un apt update) il faudra utiliser la commande nix flake update.
Pour partager notre environnement nous devrons transmettre les deux fichiers flake.nix et flake.lock.
Cette façon de faire permet d'avoir un environnement reproductible et avec des versions figées.
Plusieurs environnements dans le même flake
Vous avez peut-être remarqué que nous définissons notre environnement avec le nom devShells.default. C'est le shelle qui sera initialisé si on lance nix develop sans plus de précision.
Mais on peut être plus précis, on peut vouloir lancer un shell particulier.
Ajoutons ce bloc à la suite du devShells.default.
devShells.jupyter = pkgs.mkShell {
buildInputs = [ (pkgs.python3.withPackages (ps: with ps; [
numpy
scipy
matplotlib
pandas
jupyter
]))
];
};
Nous venons de définir un nouveau shell du nom de jupyter. Pour l'instancier nous devons lancer la commande nix develop .#jupyter.
Avoir plusieurs shells permet de séparer les choses, avoir un environnement de dev et un autre de test par exemple.
💡Avez-vous remarqué qu'on ne précise pas dans buildInputs qu'on souhaite pkgs.python3 ? Ce n'est pas nécessaire quand on utilise pkgs.python3.withPackages.
Détails du flake
Regardons d'un peu plus près notre flake. Nous avons vu qu'un flake peut proposer plusieurs sorties ou types de sorties. Comment savoir ce qu'un flake propose ?
Demandons à la commande nix flake show :
nix flake show
path:/home/laborant/dev-python?lastModified=1757923848&narHash=sha256-IXwtaHxYMEwrZyTsRdcIpSddGuZsHp6HBud9fUFE/fM%3D
└───devShells
├───aarch64-darwin
│ ├───default omitted (use '--all-systems' to show)
│ └───jupyter omitted (use '--all-systems' to show)
├───aarch64-linux
│ ├───default omitted (use '--all-systems' to show)
│ └───jupyter omitted (use '--all-systems' to show)
├───x86_64-darwin
│ ├───default omitted (use '--all-systems' to show)
│ └───jupyter omitted (use '--all-systems' to show)
└───x86_64-linux
├───default: development environment 'nix-shell'
└───jupyter: development environment 'nix-shell'
Nous voyons que notre flake propose des devShells, default et jupyter (et pour plusieurs architectures).
Comme on l'a rapidement évoqué il peut y avoir d'autres sorties, nous allons voir ça dans la prochaine partie.
Un environnement de développement Go
Dans cette partie nous continuons l'exploration des flake en créant un environnment pour le développement d'une application en Go.
Notre application écoute sur un port HTTP et affiche "Bonjour le monde !".
Le fichier main.go :
package main
import (
"fmt"
"net/http"
)
func index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<h1>Bonjour le monde !</h1>")
}
func check(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<h1>Tout va bien</h1>")
}
func main() {
http.HandleFunc("/", index)
http.HandleFunc("/health", check)
fmt.Println("Démarrage du serveur.")
http.ListenAndServe(":2000", nil)
}
Et le go.mod correspondant :
module montfort.fr/monapp
go 1.24.0
Les outils Go
Aucun outil pour développer en Go n'est installé sur la machine.
La première chose à faire est de créer un flake avec un devShell :
{
description = "mon app";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
let
systems = [ "x86_64-linux" ];
in
flake-utils.lib.eachSystem systems (
system:
let
pkgs = import nixpkgs { inherit system; };
nativeBuildInputs = with pkgs; [
go_1_24
gopls
];
buildInputs = with pkgs; [];
in {
devShells.default = pkgs.mkShellNoCC { inherit nativeBuildInputs buildInputs; };
}
);
}
On retrouve beaucoup de choses déjà vues dans le flake pour l'environnement python.
Dans un souci de clarté la liste des systèmes est limitée à l'architecture de notre machine (x86_64).
Deux nouvelles variables apparaissent nativeBuildInputs et buildInputs.
La première contient les outils nécessairent à la construction de notre application, alors que la seconde contient les programmes ou bibliothèques nécessaires à l'exécution de notre application.
Nous utilisons ces variables pour la création d'un shell (mkShellNoCC).
Ce shell ne contiendra pas d'outils de compilation en C (le NoCC) car nous allons utiliser Go.
Comme vu précédemment on utilise ce shell avec la commande nix develop.
Après avoir placé les deux fichiers main.go et go.mod dans le répertoire ~/dev-go on peut lancer l'application avec la commande :
go run main.go
Elle affiche un message de démarrage et attend d'être arrêtée par un Ctrl+C par exemple. Elle écoute sur le port 2000, chose qu'on peut vérifier avec la commande curl :
curl localhost:2000
curl localhost:2000/health
Packaging de notre application
Notre application est tellement bien que nous allons en faire un paquet pour faciliter son usage.
{
description = "mon app";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
let
systems = [ "x86_64-linux" ];
in
flake-utils.lib.eachSystem systems (
system:
let
pkgs = import nixpkgs { inherit system; };
nativeBuildInputs = with pkgs; [
go_1_24
gopls
];
buildInputs = with pkgs; [];
in {
devShells.default = pkgs.mkShellNoCC { inherit nativeBuildInputs buildInputs; };
packages.default = pkgs.buildGoModule {
name = "monapp";
src = ./.;
inherit buildInputs;
vendorHash = null;
};
})
}
Ce code définit un paquet, ce qui pour notre application correspondra à un binaire.
Il sera placé dans le répertoire result et dans l'arborescence convenue, bin/monapp.
Pour compiler notre application on utilisera la commande
nix build
Et le binaire produit pourra être lancé avec la commande :
./result/bin/monapp
Lancement de notre application
On peut déjà lancer notre application avec la commande :
nix run
La cible packages par defaut étant définie et en l'absence de cible apps, elle est utilisée.
Mais nous allons rendre les choses plus claires en ajoutant une cible apps par défaut.
Et on va aussi ajouter une cible hello pour afficher "hello world", bien sûr, mais surtout pour montrer qu'on peut avoir plusieurs cibles dans le même flake.
apps.default = {
type = "app";
program = "${self.packages.${system}.monapp}/bin/monapp";
};
apps.hello = {
type = "app";
program = "${pkgs.hello}/bin/hello";
};
Le flake.nix complet est disponible en cliquant ici.
{
description = "mon app";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
let
systems = [ "x86_64-linux" ];
in
flake-utils.lib.eachSystem systems (system:
let
pkgs = import nixpkgs { inherit system; };
nativeBuildInputs = with pkgs; [
go_1_24
gopls
jq
dive
];
buildInputs = with pkgs; [];
in rec {
devShells.monapp = pkgs.mkShellNoCC {inherit nativeBuildInputs buildInputs;};
devShells.default = devShells.monapp;
packages.default = pkgs.buildGoModule {
name = "monapp";
src = ./.;
inherit buildInputs;
vendorHash = null;
};
apps.default = {
type = "app";
program = "${self.packages.${system}.monapp}/bin/monapp";
};
apps.hello = {
type = "app";
program = "${pkgs.hello}/bin/hello";
};
}
);
}
Notre cible apps par défaut va lancer la commande donnée dans l'option program.
Analysons cette option :
selffait référence au flake en cours.packages.${system}packagesest l'ensemble des paquets fournis par ce flake.${system}est la variable définie qui représente la plateforme actuelle. Le tout sélectionne l'ensemble des paquets compilés pour l'architecture actuelle.
monapppermet d'accèder au paquet spécifiquemonapp(qu'on a déclaré précédemment)./bin/monappest le chemin vers l'exécutable dans le paquet nix.
La ligne complète pourrait ressembler à quelque chose comme ça : /nix/store/<hash>-monapp/bin/monapp
Pour l'application hello l'option programme est plus simple : pkgs est la liste des paquest nix pour notre architecture et on choisit le paquet hello.
On peut lancer les applications avec les commandes :
nix run
nix run .#default
nix run .#hello
Image docker
Il est aussi possible dans un flake de définir une sortie pour construire une image docker.
Pour cela on va déclarer un nouveau package avec le nom docker qui sera le résultat de la fonction pkgs.dockerTools.buildLayeredImage.
packages.docker = pkgs.dockerTools.buildLayeredImage {
name = "monapp";
tag = "latest";
contents = [ pkgs.hello self.packages.${system}.monapp ];
config = {
Cmd = [ "/bin/monapp" ];
ExposedPorts = {"2000/tcp" = {};};
};
};
Le contenu de notre image sera le résultat de notre paquet monapp.
On précise la commande à lancer au démarrage ainsi que le port à exposer.
On construit l'image avec la commande
nix build .#docker
L'image n'est pas automatiquement chargée dans le registre locale.
Le fichier result pointe dans l /nix/store vers l'archive tar.gz représentant l'image.
Il faut la charger dans le registre avec la commande :
docker load < result
💡On peut construire l'image et la charger en une seule commande avec docker load -i $(nix build .#docker --print-out-paths).
Pour tester notre image il faut la lancer avec
docker run -p 2000:2000 monapp
et dans un autre terminal on peut faire
curl localhost:2000
Si on veut tester notre application nix nous propose une fonctionnalité puissante pour tester une application et une configuration. Et toujours de manière reproductible et facilement distribuable
Tests
Nixpkgs fournit un environnement de test pour automatiser les tests d'intégration. Il permet de définir des tests basés sur un ensemble de configurations déclaratives de machines NixOS et d'utiliser un shell Python pour interagir avec via QEMU en tant que backend. Ces tests sont largement utilisés pour s'assurer que NixOS fonctionne comme prévu, et sont donc généralement appelés tests NixOS. Ils peuvent être écrits et lancés en dehors de NixOS, sur n'importe quelle machine Linux. Les tests d'intégration sont reproductibles grâce aux propriétés de conception de Nix, ce qui est intérsessant pour un pipeline d'intégration continue (CI).
⚠️ Le lab ne fournit pas le support de QEMU, il faudra tester sur votre machine. Mais je suis sûr que vous êtes déjà conquis par nix et en train de l'installer partout !
nixosModules.monapp = {
systemd.services.monapp = {
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
ExecStart = "${packages.monapp}/bin/monapp";
};
};
};
Pour commencer nous allons définir un module NixOS.
Un module NixOS est un composant de base du système de configuration déclaratif de NixOS Il permet de structurer et d'organiser la définition des services, des paquets ou des options système. Une configuration de machines utilise plusieurs modules qui sont évalués par Nix. Cela permet d'écrire des configurations puissantes et reproductibles.
Nous allons commencer par écrire un module très simple qui définit un service systemd qui lance notre application.
nixosModules.monapp = {
systemd.services.monapp = {
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
ExecStart = "${packages.monapp}/bin/monapp";
};
};
};
Ensuite nous allons écrire notre test. Il est constitué d'un nom, d'une définition d'une machine (ou plusieurs) et d'un script de test.
checks.monapp = pkgs.nixosTest {
name = "monapp";
nodes.machine = { config, pkgs, ... }: {
imports = [
nixosModules.monapp
];
environment.systemPackages = [ pkgs.curl ];
system.stateVersion = "25.05";
};
testScript = ''
start_all()
machine.wait_for_unit("monapp.service")
machine.wait_for_open_port(2000)
output = machine.succeed("curl localhost:2000")
assert "<h1>Bonjour le monde !</h1>" in output, f"La sortie '{output}' ne correspond pas."
'';
};
Le premier nœud machine est simple.
Il utilise NixOS version 25.05, notre module nixos monapp et dispose de la commande curl.
Enfin le test démarre les machines, attend que le service soit disponible et le port accessible et vérifie la sortie de la commande curl.
Pour lancer notre test la commande est :
nix flake check
nix flake check -L # Pour avoir les logs sur la sortie
Si tous les test se déroulent sans erreur il n'y a pas de message.
Si on besoin de débugger ou de lancer des commandes à la main on peut lancer le test en interactif.
nix run '.#checks.x86_64-linux.monapp.driverInteractive'
On se retrouve alors positionné dans le shell Python. Il faudra lancer les commandes à la main.
Commençons par
machine.start()
La machine est démarrée et une fenêtre QEMU s'ouvre. On peut se connecter en root (sans mot de passe) sur la machine.
On peut continuer à dérouler les commandes de notre script de test dans la console Python. Mais on aussi a aussi accès a l'entièreté du système au travers de la console QEMU.
Voilà pour cette approche des tests NixOS.
Conclusion
J'espère que ce tutoriel vous a intéressé et convaincu d'essayer nix et ensuite NixOS.
Le flake.nix final est disponible en cliquant ici.
{
description = "mon app";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
let
systems = [ "x86_64-linux" ];
in
flake-utils.lib.eachSystem systems (system:
let
pkgs = import nixpkgs { inherit system; };
nativeBuildInputs = with pkgs; [
go_1_24
gopls
jq
dive
];
buildInputs = with pkgs; [];
in rec {
devShells.monapp = pkgs.mkShellNoCC {inherit nativeBuildInputs buildInputs;};
devShells.default = devShells.monapp;
packages.monapp = pkgs.buildGoModule {
name = "monapp";
src = ./.;
inherit buildInputs;
vendorHash = null;
};
packages.default = packages.monapp;
packages.docker = pkgs.dockerTools.buildLayeredImage {
name = "monapp";
tag = "latest";
contents = [ pkgs.hello self.packages.${system}.monapp ];
config = {
Cmd = [ "/bin/monapp" ];
ExposedPorts = {"2000/tcp" = {};};
};
};
apps.default = {
type = "app";
program = "${self.packages.${system}.monapp}/bin/monapp";
meta = {
description = "Une courte description";
maintainers = [ "nom" ];
license = "MIT";
};
};
apps.hello = {
type = "app";
program = "${pkgs.hello}/bin/hello";
meta = {
description = "Une courte description";
maintainers = [ "nom" ];
license = "MIT";
};
};
nixosModules.monapp = {
systemd.services.monapp = {
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
ExecStart = "${packages.monapp}/bin/monapp";
};
};
};
checks.monapp = pkgs.nixosTest {
name = "monapp";
nodes.machine = { config, pkgs, ... }: {
imports = [
nixosModules.monapp
];
environment.systemPackages = [ pkgs.curl ];
system.stateVersion = "25.05";
};
testScript = ''
start_all()
machine.wait_for_unit("monapp.service")
machine.wait_for_open_port(2000)
output = machine.succeed("curl localhost:2000")
assert "<h1>Bonjour le monde !</h1>" in output, f"La sortie '{output}' ne correspond pas."
'';
};
}
);
}
About the Author
More tutorials you might like

How Container Filesystem Works: Building a Docker-like Container From Scratch
Learn how Linux containers are built from the ground up. Starting with the mount namespace and a root filesystem, see why PID, cgroup, UTS, and network namespaces naturally follow - and how this foundation makes concepts like bind mounts, volumes, and persistence in Docker or Kubernetes much easier to grasp.

How Container Networking Works: Building a Bridge Network From Scratch
Begin with the basics to understand Docker and Kubernetes networking: learn how to create and interconnect Linux network namespaces using only command-line tools.

How Servers Work: A Hands-On Introduction to TCP Sockets
Learn how servers actually work by building a tiny TCP server and client from scratch. A hands-on introduction to sockets, TCP, and the network programming model every backend, DevOps, and platform engineer should go through at least once.

Controlling Process Resources with Linux Control Groups
Learn how to limit process resources using Linux cgroups - from the most basic and labour-intensive cgroupfs manipulation to the handiest systemd-run command.
Learn by doing, not just by reading or watching
Sign up for a free account to start a VM playground right on this page, track your progress, and get notified about new learning materials.