I have been using Nix for almost four years and also maintaining some packages at nixpkgs (the primary Nix repository). The Nix learning curve is complicated, so I could not get my colleagues into it. But Devenv made it so easy to compose a developer environment, so I got their attraction, and some of them switched already, and we are looking to where we can use it too.

What is Devenv?

Devenv allows you to describe in the Nix language how your project setup has to look like for any Unix-based systems (WSL2, Mac, Linux) for amd64/arm64 architectures. As you may know, no project has the same dependencies: one needs NodeJS 16, and the other NodeJS 18 already. For those reasons, there are already solutions like NVM(node version manager) where you can pin the version using a .nvmrc file. This works, of course, only for Node, and there are similar tools for any language and devenv allows you here to pin any package and with services.

Installation of Devenv

The installation is straightforward. You will need to install the Nix package manager, Cachix, to get binary caches and not to compile stuff and devenv itself. You can follow the instructions in the docs

Install our first packages

With Devenv, we can craft a complete shell and processes with all available packages from nixpkgs instead of using for any language a version manager. Let’s say we want NodeJS 16, yarn, and PHP 8.1. Use devenv init to generate the devenv lock and the configuration. Then we can add in our new generated devenv.nix our packages.

# devenv.nix
{ pkgs, ... }: {
  packages = [ pkgs.nodejs-16_x pkgs.yarn pkgs.php81 ];
}

When we call inside that folder devenv shell and the requested packages are getting installed, we land in a Bash shell.

The shell command is limited to Bash and it’s annoying to call always the command. So I can recommend you install direnv. It hooks into your shell and calls Devenv to populate your existing shell.

You can find the package names using devenv search <term> or by visiting https://search.nixos.org.

But languages are already abstracted in Devenv, so we can toggle the tools directly without manually adding them as a package. Behind the abstraction it adds them to the packages, but for some languages like Ruby there is more happening like adjusting the GEM_HOME.

# devenv.nix
{ pkgs, ... }: {
  packages = [ pkgs.yarn ];

  languages.javascript.enable = true;
  # Uses by default the latest LTS
  languages.javascript.package = pkgs.nodejs-16_x;
  
  languages.php.enable = true;
}

If you are interested in what the options are doing, here you can find the source:

Customizing PHP configuration

We now have PHP in our environment with “default” extensions and default php.ini. Let’s configure our PHP how we want to have it.

To configure it, we have to call buildEnv on our PHP package.

# devenv.nix
{ pkgs, ... }: {
   ...
   
+  languages.php.package = pkgs.php81.buildEnv {
+    extraConfig = ''
+      memory_limit = 256m
+    '';
+  };
}

When we now open the shell again, we see the memory_limit has been changed

To install more PHP extensions like redis we can also pass more extensions inside the buildEnv

# devenv.nix
{ pkgs, ... }: {
   ...
   
  languages.php.package = pkgs.php81.buildEnv {
+   extensions = { all, enabled }: with all; enabled ++ [ redis ];
    extraConfig = ''
      memory_limit = 256m
    '';
  };
}

And we see we have Redis installed.

With enabled ++ [ redis ], we add our Redis to all currently enabled PHP extensions. We will remove all other extensions by changing it to just [ redis ]. You can find all available extensions here

Adding prebuilt processes

Typically your application needs some kind of service like Postgres, MySQL, Redis, and more. For this reason, devenv has options to start processes.

Let’s start a MySQL server:

# devenv.nix
{ pkgs, ... }: {
   ...
   
+  services.mysql.enable = true;
}

Now we have to run devenv up to start the processes in the foreground. There is a open issue that allows running in the background. This addition also installs the MySQL cli into our shell.

Typically you want to create MySQL users and databases, so it’s easier for the devs.

# devenv.nix
{ pkgs, ... }: {
   ...
   
+  services.mysql.initialDatabases = [{ name = "app"; }];
+  services.mysql.ensureUsers = [
+    {
+      name = "app";
+      password = "app";
+      ensurePermissions = { "app.*" = "ALL PRIVILEGES"; };
+    }
+  ];
+
+  # Project specific MySQL config like require always a primary key
+  services.mysql.settings.mysqld = {
+    "sql_require_primary_key" = "on";
+  };
}

So after stopping the active devenv up and restarting it we can connect with our newly created users.

By default, the MySQL service uses MySQL 8.0; you can override the package option to pkgs.mariadb to get MariaDB. (You may need to delete the existing database rm -rf .devenv/state/mysql)

The most basic services are already supported devenv:

Adding own processes

You can spawn your processes if you do not find the necessary service.

# devenv.nix
{ pkgs, ... }: {
   ...
   
+   processes.ping.exec = "ping google.com";
}

When we run devenv up, we see a new process, ping, pinging google.com.

With the power of Nix, you can also reference packages; when they are not installed, Nix will install them on the fly. Let’s use symfony-cli to start a webserver.

# devenv.nix
{ pkgs, ... }: {
   ...
   
+   # Packages refer always to the root of the package and not a specific binary
+   processes.symfony.exec = "${pkgs.symfony-cli}/bin/symfony server:start";
}

You can also use it to start your queue consumer or other things you need to run in the background.

Adding environment variables

Since we already started a MySQL server in our example, we have to tell our application the connection string for it. To simplify it, we can also set enviroment variables available in the shell and in the processes.

# devenv.nix
{ pkgs, ... }: {
   ...
   
+   env.DATABASE_URL = "mysql://app:app@localhost/app";
}

Adding scripts

It’s also possible to add a kind of shell aliases to the shell to simplify some behavior.

# devenv.nix
{ pkgs, ... }: {
   ...
   
+   scripts.build-app.exec = ''
+     npm install --prefix src/Foo
+     npm run --prefix src/Foo build
+     bin/console assets:install
+    '';
}

Tip: You can refer to pkgs like in the process example to use programs not directly installed.

Using packages from other Nix flakes

The default pkgs (nixpkgs) contains only supported software. To install older PHP versions like 7.4, you must add an additional input like phps for PHP packages.

To add an input, we have to adjust the devenv.yaml

inputs:
  nixpkgs:
    url: github:NixOS/nixpkgs/nixpkgs-unstable
  phps:
    url: github:fossar/nix-phps
    inputs:
      nixpkgs:
        follows: nixpkgs

The phps flake refers to nixpkgs, to have the same nixpkgs in phps we say to follow our root nixpkgs.

Now we can run devenv update to update our lock file and add the new input.

We must adopt our “imports” in the nix file with the change.

-{ pkgs, ... }: {
+{ pkgs, inputs, ... }:

Then we can reference it in the package option like so:

languages.php.package = inputs.phps.packages.${builtins.currentSystem}.php74;

To configure the extensions and php.ini, you can call, like before, buildEnv on it.

Editor Integration

You can find all binaries from your shell symlinked in the .devenv/profile/bin folder and use them to set up the Interpreter in PhpStorm. For Jetbrains IDEs, a plugin named Better Direnv adds support for Run Configuration. It does currently not support PhpStorm, but I made a pull request to add support for that

For VSCode users, there is also a extension for it

Cloud Developer Enviroments (CDE)

For GitHub Codespaces is a option available directly inside Devenv and for Gitpod is a pull request open to install it into the base image. So you can use the same configuration there too.

Make local changes only for you

You can create a devenv.local.nix to override configurations locally. This file should be ignored in your .gitignore (devenv init does this by default).

Devenvify an existing application

I will use the symfony demo application to show how a complete environment could look. The demo application requires the following:

  • PHP 8.1
  • Node
  • Webserver
{ pkgs, config, ... }:

{
  packages = [
    pkgs.yarn
  ];

  languages.javascript.enable = true;
  languages.php.enable = true;

  # You can use also symfony-cli like in the examples, but I like it more explict
  languages.php.fpm.pools.web = {
    settings = {
      "clear_env" = "no";
      "pm" = "dynamic";
      "pm.max_children" = 10;
      "pm.start_servers" = 2;
      "pm.min_spare_servers" = 1;
      "pm.max_spare_servers" = 10;
    };
  };

  services.caddy.enable = true;
  services.caddy.virtualHosts.":8000" = {
    extraConfig = ''
      root * public
      php_fastcgi unix/${config.languages.php.fpm.pools.web.socket}
      file_server
    '';
  };

  processes.build-assets.exec = "yarn watch";
}

The user just has to run composer install and yarn to install the packages. If you like, you can script that with the enterShell hook. enterShell is always executed when the user opens the shell or starts the processes.

enterShell = ''
    if [[ ! -d vendor ]]; then
        composer install
    fi
    
    if [[ ! -d node_modules ]]; then
        yarn
    fi
'';

Miscellaneous

The devenv.lock locks the complete package tree from your packages and their packages. It would be best if you ran devenv update from time to time to get the latest PHP/Node updates. Also, it makes sense to call devenv gc to get rid of downloaded packages which are not used anymore.

Conclusion

We started using Devenv at Shopware for 2 weeks, and some developers are moved already to it. It allows us to build one environment that works similarly on Linux, Mac, and WSL2 without any performance issues, as it runs natively without containers. The developers likes it to have a declarative way to build their environments, especially trying stuff out without breaking their complete system.

From the DevOps side, it’s also lovely when you update the config/lock. It get’s applied after the git pull of the developer, and they don’t have to think about updating their tools or pulling images.

The first commit of Devenv was two months before, so It’s a new tool making Nix more accessible for all kinds of projects outside. If you have questions or problems, feel free to join the Devenv Discord server.