Skip to main content

Dev Environments

There are nowadays like 3 major ways of providing development environments for a project.

The following table compares between

environment:

FeatureLocal DevelopmentMambaDevcontainerNix DevShell
Maintainenance Overhead⚠️ High (manual installs, dependencies)⚡ Medium (uses environment.yml)⚡ Medium (pre-defined devcontainer.json)⚡ Medium (declarative shell.nix or flake.nix).
Reproducibility❌ Low (varies per machine)⚠️ Medium (exact package versions but system-dependent)✅ High (container-based, isolated)Very high (exact dependencies and builds from source on host).
Performance✅ High (runs natively).✅ High (no container overhead, optimized package installs)⚠️ Medium (container overhead)✅ High (native execution, no container overhead).
Cross-Platform⚠️ Manual setup needed.✅ Works on Linux, macOS, Windows (via WSL)✅ Works on any container-supported OS✅ Works on any Nix-supported OS (macOS & Linux).
Isolation/Sandboxing❌ None (uses system dependencies).⚠️ Partial (isolates dependencies but modifies system env.)✅ Strong (runs in a container)⚠️ Partial (isolates dependencies but runs on host).
Ease of Use✅ Easy (if dependencies are installed).✅ Easy (Conda-like, but faster)✅ Easy (VS Code auto-setup)⚠️ Medium (requires learning Nix).
Customization✅ Full control over setup.✅ Flexible package selection via Conda/Mamba repos⚠️ Limited to what works in a container✅ Full control with Nix expressions.
Dependency Management❌ Manual✅ Automated via environment.yml✅ Automated via Dockerfile✅ Automated via Nix in flake.nix.
Works Offline✅ Yes✅ Yes (if environment is pre-installed)✅ Yes (once container is built)✅ Yes (if Nix packages are cached).
Portability❌ Difficult to share exact environment.⚠️ Medium (can share environment.yml, but OS matters)✅ Easy container can be shared✅ Flakes flake.nix are shareable.

Local Development

Never use this approach. It works on my machine is not good best-practice. This approach will break down on the long term and get in the way of collaboration and reproducibility.

Mamba

A lightweight and fast alternative package manager to Conda, ideal for managing dependencies in Python and data science projects. It provides isolated environments and cross-platform support but lacks full system-level isolation. While easier to set up than Nix, its reproducibility depends on careful version pinning and OS compatibility. You may run in to the issue that you need multiple environments because of dependency resolution problems. A flaw that only Nix DevShell do not have by design.

Devcontainer

Best if you want an isolated and shareable setup. It can be used also without VS code IDE. Its reproducible only to a certain degree as the Container file (e.g. Dockerfile or Containerfile) needs to be meticulously crafted (version pinning) to make it more stable over container rebuilds.

Nix DevShell

Best if you need true reproducibility without containers and prefer a declarative approach. It can act as a base layer to provide a .devcontainer setup, which bundles the Nix DevShell into a container.

There are different flavors and projects which help circumventing the Nix learning curve (which is pretty steep) in this manner:

  • Pure Nix DevShell: This is not suggested for starting as the pkgs.mkShell function in nixpkgs works but is more bare-metal.

  • devenv.sh: This is used in the repository templates. We use this as learning Nix a useful skill, and this will get you moving along quicker in the Nix ecosystem.

  • devbox: A simple company-backed CLI tool providing Nix-backed environments. The repository templates also provide this setup to choose from which is simpler to start with (this is a TODO).

  • flox.dev: Similar to devbox, with toml config and slightly less mature (as of 2025).

Usage in CI

Devcontainer or Nix DevShell setups have good CI integration on Github Actions (and other systems) as the jobs in Github Actions run on a VM and the provided actions will take care of the correct setup etc:

Notes

  • Running Devcontainer setup in another CI might require nested containers (at least 1-layer deep) which is tricky to get right, especially when you are using CI as a service which only runs Jobs containerized and you don't maintain your own CI runners.

  • Running a Nix DevShells in CI is extremely easy when Nix is available on the VM or in a container. One minor downside of using Nix is that you need Cachix or something similar to provide a fast cache for the Nix store /nix/store which can get large due to strong isolated dependencies. However, the strong isolation guarantee far outweighs this issue.

Pinning Versions

Devcontainer

With a .devcontainer/devcontainer.json setup you pin versions by crafting your Containerfile such that it only downloads a certain version of e.g. python. Also using Devcontainer features gives you some degree of version pinning in .devcontainer/devcontainer.json:

{
"features": {
"ghcr.io/devcontainers/features/node:1": {p
"version": "18"
}
}
}

Pinning versions in your own Containerfile (or Dockerfile) used in the .devcontainer/devcontainer.json happens on different level:

Nix DevShell

  • Using devenv.sh or flox.dev: They come with their own wrapper around the Nix packages and therefore also have version pinning in their respective manifest file which define the development environment.

  • When using devenv.sh which can be more close to the Nix language as you write Nix code instead of TOML/JSON as with devenv.sh or flox.dev To version pin certain derivations you proceed as described here..