Dans mon article précédent sur Nix, j’expliquais pourquoi j’avais progressivement abandonné asdf et Mise au profit de Nix. Puis, nous avons vu comment sécuriser nos secrets avec sops-nix.
Aujourd’hui, on s’attaque au nerf de la guerre du dev au quotidien : l’environnement de travail par projet.
On a tous connu ce scénario :
- Le projet A a besoin de Node 18 et d’une version spécifique de pnpm.
- Le projet B tourne sous Node 22 avec Python 3.12 et des bindings C compilés.
- Le projet C exige Hugo version extended, un linter Markdown et un client PostgreSQL.
Résultat habituel ? Des conflits de versions partout, des nvm use oubliés,
des installations globales qui polluent l’OS, ou le recours à de lourds
conteneurs Docker/devcontainers juste pour formater du code ou lancer un test.
Avec Nix Flakes combiné à direnv, on résout tout ça d’un coup : dès qu’on entre dans le dossier d’un projet, les bons outils sont là. Quand on en sort, ils disparaissent. Magique ? Presque, c’est juste déclaratif.
Le problème avec nix develop tout seul
Quand on utilise un Flake Nix, la commande native pour entrer dans un environnement de dev est :
nix develop
C’est très puissant, mais au quotidien, ça a deux défauts majeurs :
- Il faut y penser : à chaque fois qu’on ouvre un terminal ou qu’on fait
un
cddans le projet, il faut retaper la commande. - C’est un sous-shell : taper
exitne ferme pas votre terminal, ça vous sort juste du shell Nix. Et si vous lancez votre IDE ou un script externe, il n’hérite pas forcément de cet environnement.
C’est là qu’intervient direnv.
direnv + nix-direnv : le duo gagnant
direnv est un utilitaire génial pour votre shell
(bash, zsh, fish). Son rôle est simple : charger automatiquement des
variables d’environnement quand vous entrez dans un dossier contenant un
fichier .envrc, et les décharger quand vous le quittez.
Mais attention : si on utilise simplement le hook Nix de base dans direnv,
l’évaluation du Flake à chaque cd peut prendre une ou deux secondes.
C’est agaçant.
Pour rendre l’expérience instantanée, il faut installer nix-direnv.
Il ajoute un mécanisme de cache intelligent : l’environnement n’est
réévalué que si votre fichier flake.nix ou flake.lock a bougé !
Installation rapide
Si vous êtes sous NixOS ou utilisez Home-Manager, cela tient en deux lignes :
programs.direnv = {
enable = true;
nix-direnv.enable = true;
};
Sur d’autres distributions ou macOS, suivez simplement les instructions du dépôt officiel nix-direnv.
Une fois installé, il suffit d’un fichier .envrc d’une seule ligne à la racine
de votre projet :
use flake
La première fois, direnv bloque l’exécution par sécurité. Il suffit de taper :
direnv allow
Et voilà ! Désormais, chaque cd dans votre projet charge instantanément vos
outils dans votre shell principal.
Cas 1 : Le starter kit universel
Voyons à quoi ressemble un flake.nix minimaliste que vous pouvez copier-coller
dans n’importe lequel de vos dépôts.
{
description = "Mon environnement de développement minimaliste";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs }:
let
# Systèmes supportés (Linux Intel/ARM, macOS Intel/Apple Silicon)
allSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f {
pkgs = import nixpkgs { inherit system; };
});
in
{
devShells = forAllSystems ({ pkgs }: {
default = pkgs.mkShell {
packages = with pkgs; [
git
curl
jq
];
};
});
};
}
Quelques remarques sur ce modèle :
pkgs.mkShellest la fonction magique de Nix conçue spécifiquement pour fabriquer des shells de développement.- La variable
packagesliste tous les binaires qui seront injectés dans votre$PATH. - Le fichier
flake.lockgénéré garantit que chaque personne qui clone le dépôt aura au bit près les mêmes versions degit,curletjq.
Cas 2 : En situation réelle, l’exemple de ce blog
Passons à un cas concret : le flake.nix utilisé pour faire tourner et builder
ce blog Hugo !
Dans un projet de blog ou de documentation, on ne veut pas seulement des runtimes. On veut aussi des linters et des commandes personnalisées simples pour l’équipe ou la CI.
Voici un extrait de ce qu’on utilise ici :
{
description = "Julien Noblet's blog development environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs }:
let
allSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f {
pkgs = import nixpkgs { inherit system; };
});
in
{
devShells = forAllSystems ({ pkgs }: {
default = pkgs.mkShell {
packages = with pkgs; [
hugo
git
markdownlint-cli2
# Des scripts utilitaires déclarés directement en Nix !
(writeShellScriptBin "build-hugo" ''
exec hugo --gc --minify "$@"
'')
(writeShellScriptBin "test-all" ''
echo "==> Running Hugo build..."
hugo --gc --minify
echo "==> Running markdown linting..."
markdownlint-cli2
'')
];
shellHook = ''
echo "--------------------------------------------------"
echo " Bienvenue dans l'environnement du blog ! "
echo " Hugo: $(hugo version | cut -d' ' -f2)"
echo " Git: $(git --version | cut -d' ' -f3)"
echo "--------------------------------------------------"
echo " Commandes disponibles :"
echo " build-hugo - Construit le site en mode optimisé"
echo " test-all - Valide le build et passe le linter"
echo "--------------------------------------------------"
# Initialisation automatique des sous-modules Git si besoin
if [ -d .git ] && [ ! -f "themes/PaperMod/theme.toml" ]; then
echo "Initialisation des sous-modules..."
git submodule update --init --recursive
fi
'';
};
});
};
}
Notez l’astuce de writeShellScriptBin : plus besoin de maintenir un vieux
Makefile ou des scripts bash éparpillés à la racine du projet. Vos commandes
de build et de test sont packagées avec leurs dépendances et directement
accessibles dans le terminal.
Et le shellHook s’exécute automatiquement dès que direnv s’active, pratique
pour vérifier l’état du dépôt ou afficher un mémo des commandes.
Cas 3 : Projet polyglotte & multi-runtimes
C’est ici que Nix enterre définitivement les alternatives comme asdf ou les gestionnaires par langage (nvm, pyenv, rbenv, gvm…).
Imaginez une application complète :
- Un backend en Go ou Python.
- Un frontend en Node.js avec pnpm.
- Une base de données locale ou un client natif (PostgreSQL).
- Des bibliothèques C système (OpenSSL, zlib) nécessaires pour compiler certains modules natifs.
Avec un gestionnaire traditionnel, vous devez installer les outils un par un, prier pour que les headers C correspondent à ce que votre distribution fournit, et configurer manuellement vos variables d’environnement.
Avec Nix, tout tient dans une seule déclaration :
devShells = forAllSystems ({ pkgs }: {
default = pkgs.mkShell {
packages = with pkgs; [
# Runtimes
go_1_24
nodejs_22
nodePackages.pnpm
python312Full
# Outils CLI et bases de données
postgresql_16
sqls
jq
# Bibliothèques système
openssl
pkg-config
];
# Variables d'environnement injectées dans le shell
DATABASE_URL = "postgres://postgres:postgres@localhost:5432/app_dev";
PORT = "8080";
shellHook = ''
# Configuration locale pour les outils qui écrivent des caches
export GOPATH="$PWD/.cache/go"
export PATH="$GOPATH/bin:$PATH"
# Pour que pkg-config et les bindings C trouvent OpenSSL
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig:$PKG_CONFIG_PATH"
echo "Environnement polyglotte prêt !"
echo "Go: $(go version)"
echo "Node: $(node -v)"
'';
};
});
Vous changez de machine ou un nouveau développeur arrive sur le projet ?
Il tape git clone, fait un cd, valide avec direnv allow, et tout
fonctionne immédiatement. Aucun “chez moi ça marchait”.
Les bonnes pratiques au quotidien
Voici quelques astuces issues du terrain pour profiter de ce setup sans accroc :
1. Versionner flake.lock, ignorer .direnv
Le fichier flake.lock doit être commité dans Git : c’est lui qui verrouille
les commits précis de nixpkgs et garantit la reproductibilité.
En revanche, n’oubliez pas d’ajouter .direnv à votre .gitignore :
c’est le répertoire local où direnv stocke ses liens de cache.
2. Mettre à jour ses dépendances
Pour mettre à jour tous vos outils vers les dernières versions stables ou instables fournies par votre canal :
nix flake update
Comme pour un package-lock.json, testez ensuite votre projet avant de pusher
la mise à jour du flake.lock.
3. Intégration IDE (VSCode / Neovim)
Pour que vos extensions d’éditeurs (LSP Go, ESLint, TypeScript, gopls…) trouvent les bons binaires, installez l’extension direnv dans votre éditeur (ex: direnv sur VSCode). Elle appliquera automatiquement les variables d’environnement au workspace sans configuration supplémentaire.
4. Empêcher le garbage collector d’effacer vos devshells
Par défaut, nix-collect-garbage supprime tout ce qui n’est pas référencé par
votre système principal. Heureusement, nix-direnv enregistre automatiquement
des “racines GC” (gcroots) dans .direnv/ pour que vos environnements de dev
actifs ne soient pas purgés lors d’un nettoyage.
En résumé
Associer Nix Flakes et direnv, c’est s’offrir le meilleur des deux mondes :
- La rigueur et la reproductibilité absolue de Nix.
- La simplicité invisible et sans friction de direnv au quotidien.
Plus besoin de jongler entre dix gestionnaires de versions différents ni de lourds conteneurs pour travailler sereinement.
Et vous, quel est votre setup pour vos environnements de dev par projet ? N’hésitez pas à partager vos retours ou vos astuces !