Recreating development environment with Nix Flake
In this post, I will show you how to define and recreate development environment of your projects anywhere using Nix Flake.
Create your development environment with nix develop
You can define the development environment of your project in Nix Flake under devShells
with pkgs.mkShell
, and a shell based on that environment can be created with nix develop .#<environment-name>
. For the packages you want to include in this environment, you can define them in nativeBuildInputs
, and they will be loaded through the PATH
in the shell automatically.
The following example is a flake that install kustomize
in devShells.default
. flake-parts
is used to simplify configuration, so you can define devShells.<name>
instead of devShells.<system>.<name>
for each system.
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
flake-parts.url = "github:hercules-ci/flake-parts";
};
outputs = inputs@{ self, nixpkgs, flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" ];
perSystem = { config, pkgs, system, ... }:
let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [ ];
};
in {
devShells.default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [
kustomize
];
};
};
};
}
If you want to define environment variables in this environment or trigger some actions when entering it, for example start a database process in background, you can define those commands in shellHook
.
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
flake-parts.url = "github:hercules-ci/flake-parts";
};
outputs = inputs@{ self, nixpkgs, flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" ];
perSystem = { config, pkgs, system, ... }:
let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [ ];
};
in {
devShells.default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [
kustomize
];
shellHook = ''
set -a;
export FOO="bar";
set +a;
'';
};
};
};
}
You might have also seen snippets of pkgs.mkShell
that defines what packages to use through buildInputs
or packages
. buildInputs
is interchangeable with nativeBuildInputs
, unless you are cross compiling, as packages defined in it are for the foreign platform that your compilation is targeting, and packages defined in nativeBuildInputs
are for the native platform where the compilation will happen. packages
is just an alias argument for nativeBuildInputs
, as it will be merged with the nativeBuildInputs
at the end.
Under the hood, pkgs.mkShell
creates a derivation that only has a build
phrase, which does not do anything on its own, and it is meant to be used only with the old nix-shell
command, that is wrapped by nix develop
.
Automate environment creation with direnv
Instead of running nix develop
manually every time, what if there is a tool that can automate it for you, whenever you navigate into that project? direnv
is a tool that does exactly that. With its Nix Flake integration, nix-direnv
, it would call nix develop
for you when you cd
into a project, and the PATH
and environment variables in your current shell will be updated temporarily, until you navigate away from that project. This is a small but important improvement for me, as I don’t have to use a bare-boned bash
shell and I can continue to enjoy the custom prompt and key bindings in my current shell profile.
There are a few ways to install nix-direnv
in your system. Assuming you are using Home Manager and you are using zsh
as your shell, you would create a module as the following. Similar configuration exists for bash
and fish
as well.
{ inputs, lib, config, pkgs, system, isDarwin, ... }: {
programs.zsh = {
enable = true;
};
programs.direnv.enable = true;
programs.direnv.enableZshIntegration = true;
programs.direnv.nix-direnv.enable = true;
}
Then in the project you want to enable nix-direnv
support, you need to create a file called .envrc
with use flake
.
use flake
Instead of the default devShells
, you can define and run a specific devShells
. In the following example, we instruct direnv
to run devShells.<system>.ci
automatically for us.
use flake .#ci
Finally, you will need to run direnv allow
once at the project level to allow direnv
to do its magic. The next time you navigate into this project, your development environment will be recreated automatically by direnv
.
Reproduce your development environment anywhere with Docker
Since our development environment is defined in Nix Flake, we can reproduce our environment on any machine with Nix Flake support, including a Docker image. The Nix community provides a Docker image for running Nix Flake, and we can recreate our project environment and execute commands in it.
I found it very useful to bring my environment from Nix Flake into a CI/CD pipeline through Docker. Not only the version of packages I get are identical with what I have locally, but also I don’t need to define the installation process for each of them again. Everything is controlled in the Nix Flake.
Since we cannot interact with a shell started by nix develop
in a pipeline, we have to pass the command and arguments that we want to execute to nix develop
as nix develop -c <command> [...args]
. The following example is a task for Tekton pipeline that executes kustomize build --enable-helm ./final
in an environment created by nix develop
.
---
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: generate-manifest
spec:
steps:
- image: docker.io/nixpkgs/nix-flakes:nixos-24.11
name: generate-manifest
script: |
nix develop -c kustomize build --enable-helm ./final;