Skip to content

devenv 1.9: Scaling Nix projects using modules and profiles

Profiles are a new way to organize and selectively activate parts of development environment.

While we try our best to ship sane defaults for languages and services, each team has its own preferences. We're still working on uniform interface for language configuration so you'll be able to customize each bit of the environment.

Typically, these best practices are created using scaffolds, these quickly go out of date and don't have the ability to ship updates in a central place.

On top of that, when developing in a repository with different components, it's handy to be able to activate only part of the development environment.

Extending devenv modules

Teams can define their own set of recommended best practices in a central repository to create even more opinionated environments:

devenv.nix
{ lib, config, pkgs, ... }: {
  options.myteam = {
    languages.rust.enable = lib.mkEnableOption "Rust development stack";
    services.database.enable = lib.mkEnableOption "Database services";
  };

  config = {
    packages = lib.mkIf config.myteam.languages.rust.enable [
      pkgs.cargo-watch
    ];

    languages.rust = lib.mkIf config.myteam.languages.rust.enable {
      enable = true;
      channel = "nightly";
    };

    services.postgres = lib.mkIf config.myteam.services.database.enable {
      enable = true;
      initialScript = "CREATE DATABASE myapp;";
    };
  };
}

We have defined our defaults for myteam.languages.rust and myteam.services.database.

Using Profiles

Once you have your team module defined, you can start using it in new projects:

devenv.yaml
inputs:
  myteam:
    url: github:myorg/devenv-myteam
    flake: false
inputs:
- myteam

This automatically includes your centrally managed module.

Since options default to false, you'll need to enable them per project. You can enable common defaults globally and use profiles to activate additional components on demand:

devenv.nix
{ pkgs, config, ... }: {
  packages = [ pkgs.jq ];

  profiles = {
    backend.module = {
      myteam.languages.rust.enable = true;
      myteam.services.database.enable = true;
    };

    frontend.module = {
      languages.javascript.enable = true;
    };

    fullstack.extends = [ "backend" "frontend" ];
  };
}

Let's do some Rust development with the base configuration:

$ devenv --profile backend shell

Using backend profile to launch the database:

$ devenv --profile backend up

Using frontend profile for JavaScript development:

$ devenv --profile frontend shell

Using fullstack profile to get both backend and frontend tools (extends both profiles):

$ devenv --profile fullstack shell

The fullstack profile automatically includes everything from both the backend and frontend profiles through extends. Use ad-hoc environment options to further customize:

$ devenv -P fullstack -O myteam.languages.rust.enable:bool false shell

User and Hostname Profiles

Profiles can activate automatically based on hostname or username:

{
  profiles = {
    hostname."dev-server".module = {
      myteam.services.database.enable = true;
    };

    user."alice".module = {
      myteam.languages.rust.enable = true;
    };
  };
}

When user alice runs devenv shell on dev-server hostname, both her user profile and the hostname profile automatically activate.

This gives teams fine-grained control over development environments while keeping individual setups simple and centralized.

Profile priorities

To keep profile-heavy projects from fighting each other we wrap every profile module in an automatic override priority. The base configuration is applied first, hostname profiles stack on top, then user profiles, and finally any manual --profile flags—if you pass several, the last flag wins. Extends chains apply parents before children so overrides land where you expect.

Here is a simple example where every tier toggles the same option, yet the final value stays deterministic:

{ config, ... }: {
  myteam.services.database.enable = false;

  profiles = {
    hostname."dev-server".module = {
      myteam.services.database.enable = true;
    };

    user."alice".module = {
      myteam.services.database.enable = false;
    };

    qa.module = {
      myteam.services.database.enable = true;
    };
  };
}

Alice starting a shell on dev-server will see the base configuration turn the database off, the hostname profile enable it, her user profile disable it again, and a manual devenv --profile qa shell flip it back on. Even with conflicting assignments, priorities make the outcome predictable and avoid merge conflicts.

Building Linux containers on macOS

Oh, we've also removed restriction so you can now build containers on macOS if you configure a linux builder.

Containers are likely to get a simplification redesign, as we've learned a lot since they were introduced in devenv 0.6.

Getting Started

New to devenv? Start with the getting started guide to learn the basics.

Check out the profiles documentation for complete examples.

Join the devenv Discord community to share how your team uses profiles!

Domen