Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cachix/devenv/llms.txt

Use this file to discover all available pages before exploring further.

Nix Flakes provide a standardised way to manage Nix projects — pinning dependencies in a lock file and producing structured outputs. devenv can be consumed as a flake module so that its full language/process/service module system becomes available inside an existing flake.nix, or via flake-parts for a more modular multi-system configuration.
If you are new to devenv and Nix, starting with the standard devenv.nix + devenv.yaml approach will give you the smoothest experience. The flake integration is aimed at experienced Nix users with an existing flake-based ecosystem.

When to Use Flakes vs the devenv CLI

For most projects we recommend using the dedicated devenv CLI. However, the flake integration makes sense when:
  • You already maintain a flake-based project ecosystem.
  • Your dev shell needs to be consumed as a devShell output by downstream flakes.
  • You are an experienced Nix user comfortable with flake evaluation semantics.

Feature Comparison

Featuredevenv CLINix Flakes
External flake inputs
Shared remote configs
Designed for developer environments
Built-in container support
Protection from garbage collection
Faster evaluation (lazy trees)
Evaluation caching
Pure evaluation❌ (impure by default)
Cross-project references
secretspec.dev
Running processes when testing

Option A: Plain Nix Flakes

Initialise from the Template

$ nix flake init --template github:cachix/devenv
This creates a flake.nix with a basic devenv configuration and an optional .envrc for direnv support.

Minimal flake.nix

The shell is defined as a devShell output built with devenv.lib.mkShell:
{
  inputs = {
    nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling";
    devenv.url = "github:cachix/devenv";
  };

  nixConfig = {
    extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
    extra-substituters = "https://devenv.cachix.org";
  };

  outputs = { self, nixpkgs, devenv, ... } @ inputs:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
    in
    {
      devShells.${system}.default = devenv.lib.mkShell {
        inherit inputs pkgs;
        modules = [
          ({ pkgs, config, ... }: {
            # This is your devenv configuration
            packages = [ pkgs.hello ];

            enterShell = ''
              hello
            '';

            processes.run.exec = "hello";
          })
        ];
      };
    };
}

Entering the Shell

$ nix develop --no-pure-eval
Flakes use pure evaluation by default, which prevents devenv from querying the working directory. The --no-pure-eval flag relaxes this restriction. Alternatively, you can override devenv.root to an absolute path, but that makes the flake non-portable.

Launching Processes and Running Tests

Once inside the shell, use the standard devenv commands:
$ devenv up
17:34:37 system | run.1 started (pid=1046939)
17:34:37 run.1  | Hello, world!
17:34:37 system | run.1 stopped (rc=0)
$ devenv test
Running tasks     devenv:enterShell
Succeeded         devenv:git-hooks:install 10ms
Succeeded         devenv:enterShell         4ms
2 Succeeded                                 14.75ms

Multiple Shells in One Flake

For monorepos or projects with multiple components, define several devShells:
outputs = { self, nixpkgs, devenv, ... } @ inputs:
  let
    system = "x86_64-linux";
    pkgs = nixpkgs.legacyPackages.${system};
  in
  {
    devShells.${system} = {
      projectA = devenv.lib.mkShell {
        inherit inputs pkgs;
        modules = [
          {
            enterShell = ''
              echo this is project A
            '';
          }
        ];
      };

      projectB = devenv.lib.mkShell {
        inherit inputs pkgs;
        modules = [
          {
            enterShell = ''
              echo this is project B
            '';
          }
        ];
      };
    };
  };
Enter a named shell with:
$ nix develop --no-pure-eval .#projectA
this is project A
(devenv) $

External Flakes

If you cannot add a flake.nix to the project repository, create a central flake in a separate repo and reference it:
$ nix develop --no-pure-eval file:/path/to/central/flake#projectA
this is project A
(devenv) $
External flake references also work with github: and git: URLs. See Nix flake references for the full syntax.

Automated Shell Switching with direnv

1

Install nix-direnv

2

Download the .envrc template

$ curl -o .envrc https://raw.githubusercontent.com/cachix/devenv/main/templates/flake/.envrc
3

Allow direnv

$ direnv allow
When direnv manages the shell, devenv up, devenv test, and devenv tasks skip re-evaluation and use the cached environment, starting significantly faster.

Option B: flake-parts

flake-parts provides a modular framework for organising flakes across multiple systems. devenv ships a flakeModule you can import directly.

Initialise from the Template

$ nix flake init --template github:cachix/devenv#flake-parts

Minimal flake.nix with flake-parts

{
  inputs = {
    flake-parts.url = "github:hercules-ci/flake-parts";
    devenv.url = "github:cachix/devenv";
    nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling";
  };

  outputs = inputs@{ flake-parts, nixpkgs, ... }:
    flake-parts.lib.mkFlake { inherit inputs; } {
      imports = [
        inputs.devenv.flakeModule
      ];
      systems = nixpkgs.lib.systems.flakeExposed;

      perSystem = { config, self', inputs', pkgs, system, ... }: {
        packages.default = pkgs.hello;

        devenv.shells.default = {
          # https://devenv.sh/reference/options/
          packages = [ config.packages.default ];

          enterShell = ''
            hello
          '';
        };
      };
    };
}
A single shell is defined for every system listed under systems. The devenv configuration lives under devenv.shells.<name> inside perSystem.

Importing a devenv Module

You can split your devenv configuration into separate files and import them:
# inside perSystem = { config, ... }: {

devenv.shells.default = {
  imports = [ ./devenv-foo.nix ];

  services.foo.package = config.packages.foo;

  enterShell = ''
    hello
  '';
};
The imported module can declare options and wire them up without needing to know about the flake:
{ config, lib, ... }:
let cfg = config.services.foo;
in {
  options = {
    services.foo = {
      package = lib.mkOption {
        type = lib.types.package;
        defaultText = lib.literalMD "defined internally";
        description = "The foo package to use.";
      };
    };
  };
  config = lib.mkIf cfg.enable {
    processes.foo.exec = "${cfg.package}/bin/foo";
  };
}

Multiple Shells with flake-parts

# inside perSystem = { config, ... }: {

devenv.shells.projectA = {
  packages = [ config.packages.default ];
  enterShell = ''
    echo this is project A
    hello
  '';
};

devenv.shells.projectB = {
  packages = [ config.packages.default ];
  enterShell = ''
    echo this is project B
    hello
  '';
};

# Make projectA the default
devShells.default = config.devShells.projectA;
$ nix develop --no-pure-eval .#projectA
this is project A
(devenv) $
$ nix develop --no-pure-eval .
this is project A
(devenv) $

Build docs developers (and LLMs) love