Devenv: Reproducible Development Environments with Nix
Devenv: Reproducible Development Environments with Nix

Every developer has lived this experience: you clone a repository, follow the README, and spend the next two hours installing the right version of Python, the correct PostgreSQL release, a specific version of protoc, and three system libraries that the project silently depends on. The README says "works on my machine" and it does -- on the original author's machine, configured exactly the way they had it eighteen months ago.
Devenv solves this by letting you declare your entire development environment in a single file. It uses Nix under the hood -- the most powerful package manager ever built -- but wraps it in an approachable configuration format that does not require you to learn the Nix language. You declare which packages you need, which services to run, which environment variables to set, and which scripts to expose. When a teammate runs devenv shell, they get an identical environment regardless of whether they are on macOS, Linux, or NixOS.
Why Devenv?
The development environment problem has several attempted solutions, each with significant trade-offs.
Docker dev containers give you reproducibility but at a cost: file system mounts are slow on macOS, GPU passthrough is painful, and the feedback loop for changing dependencies requires rebuilding layers. You are developing inside a virtualized Linux environment that does not match your host OS.
Version managers (nvm, pyenv, rbenv) solve one language at a time. A real project needs Node, Python, PostgreSQL, Redis, and a dozen CLI tools. Coordinating six different version managers is fragile.
Nix flakes give you everything but require learning the Nix language, which has a steep learning curve. Writing a proper flake with devShells, overlays, and nixpkgs pinning is not something most developers want to do on day one.
Devbox by Jetify is the closest competitor. It also wraps Nix in a simpler interface. Devenv differs by offering integrated process management, service definitions (databases, queues), pre-commit hook support, language-specific module systems, and deeper Nix integration for those who want to drop down a level.
Devenv hits the sweet spot: Nix's power with a configuration experience closer to Docker Compose.
Installation
Devenv requires the Nix package manager. If you do not have Nix installed, Devenv's installer handles it:
# Install Devenv (installs Nix automatically if needed)
curl -fsSL https://devenv.sh/install.sh | bash
If you already have Nix with flakes enabled:
# Using Nix directly
nix profile install --accept-flake-config github:cachix/devenv/latest
Verify the installation:
devenv version
Cachix Binary Cache
Devenv uses Cachix to serve pre-built binaries so you do not compile everything from source. The installer configures this automatically. If you are on a team, you can set up a private Cachix cache to share builds:
cachix use your-team-cache
Your First devenv.nix
Initialize a project:
cd your-project
devenv init
This creates two files: devenv.nix (your environment definition) and devenv.yaml (input sources). The devenv.nix file is where you spend most of your time:
{ pkgs, ... }:
{
# Packages available in the environment
packages = [
pkgs.git
pkgs.curl
pkgs.jq
];
# Environment variables
env.DATABASE_URL = "postgresql://localhost:5432/myapp";
env.RUST_LOG = "debug";
# Shell hook -- runs when entering the environment
enterShell = ''
echo "Welcome to the project dev environment"
echo "Node: $(node --version)"
echo "Python: $(python --version)"
'';
}
Enter the environment:
devenv shell
This drops you into a shell with all declared packages available on your PATH, environment variables set, and the shell hook executed. When you exit, everything is gone -- no global pollution.
Language Support
Devenv has first-class modules for most popular languages. These handle not just the runtime but also package managers, build tools, and language servers.
Python Stack
{ pkgs, ... }:
{
languages.python = {
enable = true;
version = "3.12";
# Virtual environment managed by Devenv
venv.enable = true;
venv.requirements = ./requirements.txt;
};
packages = [
pkgs.ruff # Linter
pkgs.black # Formatter
];
}
Node.js Stack
{ pkgs, ... }:
{
languages.javascript = {
enable = true;
package = pkgs.nodejs_22;
# Or use Bun instead
# bun.enable = true;
};
# TypeScript support
languages.typescript.enable = true;
packages = [
pkgs.nodePackages.pnpm
];
}
Rust Stack
{ pkgs, ... }:
{
languages.rust = {
enable = true;
channel = "stable"; # or "nightly"
# Components to include
components = [
"rustc"
"cargo"
"clippy"
"rustfmt"
"rust-analyzer"
];
};
packages = [
pkgs.pkg-config
pkgs.openssl
];
}
Multi-Language Projects
Real projects often span multiple languages. Devenv handles this naturally:
{ pkgs, ... }:
{
languages.python = {
enable = true;
version = "3.12";
};
languages.javascript = {
enable = true;
package = pkgs.nodejs_22;
};
languages.rust.enable = true;
packages = [
pkgs.protobuf # For gRPC definitions
pkgs.grpcurl # gRPC testing
pkgs.docker-compose # Container orchestration
];
}
Process Management
Devenv includes a built-in process manager powered by process-compose. This replaces the need for tools like Foreman, Overmind, or custom Makefiles that start multiple services:
{ pkgs, ... }:
{
processes = {
backend.exec = "cd backend && cargo watch -x run";
frontend.exec = "cd frontend && npm run dev";
worker.exec = "cd worker && python celery_worker.py";
tailwind.exec = "npx tailwindcss -i input.css -o output.css --watch";
};
}
Start all processes:
devenv up
This launches all processes with a TUI that shows logs for each service, supports restarting individual processes, and handles signal propagation for clean shutdown.
Services: Databases and Infrastructure
One of Devenv's strongest features is integrated service management. Instead of asking developers to install PostgreSQL globally or run Docker containers separately, you declare services directly:
PostgreSQL
{ pkgs, ... }:
{
services.postgres = {
enable = true;
package = pkgs.postgresql_16;
listen_addresses = "127.0.0.1";
port = 5432;
initialDatabases = [
{ name = "myapp_dev"; }
{ name = "myapp_test"; }
];
initialScript = ''
CREATE USER myapp WITH PASSWORD 'dev_password';
GRANT ALL PRIVILEGES ON DATABASE myapp_dev TO myapp;
'';
};
}
Redis
{
services.redis = {
enable = true;
port = 6379;
};
}
Multiple Services Together
{ pkgs, ... }:
{
services.postgres = {
enable = true;
package = pkgs.postgresql_16;
initialDatabases = [{ name = "app"; }];
};
services.redis.enable = true;
services.minio = {
enable = true;
buckets = ["uploads" "backups"];
};
services.mailpit = {
enable = true; # Local email testing
};
services.rabbitmq.enable = true;
}
When you run devenv up, all services start automatically with data directories inside .devenv/state/. No global installation, no port conflicts between projects, no leftover data from other projects.
Pre-Commit Hooks
Devenv integrates directly with the pre-commit framework. Instead of maintaining a separate .pre-commit-config.yaml, you declare hooks in your devenv.nix:
{ pkgs, ... }:
{
pre-commit.hooks = {
# Formatting
nixpkgs-fmt.enable = true;
prettier.enable = true;
black.enable = true;
rustfmt.enable = true;
# Linting
clippy.enable = true;
ruff.enable = true;
shellcheck.enable = true;
# Security
detect-private-key.enable = true;
# Custom hook
my-check = {
enable = true;
entry = "${pkgs.bash}/bin/bash -c 'echo checking...'";
files = "\\.rs$";
language = "system";
};
};
}
The hooks are installed automatically when entering the shell. No separate pre-commit install step needed.
Scripts and Tasks
Define project-specific scripts that are available as commands in the shell:
{ pkgs, ... }:
{
scripts = {
db-reset.exec = ''
dropdb --if-exists myapp_dev
createdb myapp_dev
python manage.py migrate
python manage.py seed
echo "Database reset complete"
'';
lint.exec = ''
ruff check .
mypy src/
prettier --check "**/*.{ts,tsx}"
'';
deploy.exec = ''
echo "Deploying to $DEPLOY_ENV..."
docker build -t myapp .
docker push registry.example.com/myapp:latest
'';
};
}
These scripts are available as commands immediately:
$ devenv shell
$ db-reset
Database reset complete
$ lint
All checks passed!
Comparison with Alternatives
| Feature | Devenv | Devbox | Docker Dev Containers | Nix Flakes |
|---|---|---|---|---|
| Nix-powered | Yes | Yes | No | Yes |
| Learning curve | Low | Low | Medium | High |
| Service management | Built-in | Via plugins | Docker Compose | Manual |
| Process manager | Built-in | Via plugins | Manual | Manual |
| Pre-commit hooks | Built-in | No | Manual | Manual |
| Language modules | Rich | Basic | N/A | Manual |
| macOS performance | Native | Native | Slow (VM) | Native |
| Team sharing | devenv.nix in repo | devbox.json in repo | .devcontainer in repo | flake.nix in repo |
| Escape hatch to Nix | Full | Limited | N/A | N/A (is Nix) |
| Container support | devenv container | Yes | Native | Manual |
| Caching | Cachix | Jetify Cloud | Docker layers | Cachix |
The devenv.yaml File
While devenv.nix defines your environment, devenv.yaml defines where Nix packages come from:
inputs:
nixpkgs:
url: github:NixOS/nixpkgs/nixpkgs-unstable
nixpkgs-stable:
url: github:NixOS/nixpkgs/nixos-24.11
You can pin specific revisions for complete reproducibility:
inputs:
nixpkgs:
url: github:NixOS/nixpkgs/nixpkgs-unstable
override:
rev: abc123def456 # Pin to exact commit
Use packages from different channels:
{ pkgs, inputs, ... }:
let
pkgs-stable = inputs.nixpkgs-stable.legacyPackages.${pkgs.system};
in {
packages = [
pkgs.nodejs_22 # Latest from unstable
pkgs-stable.postgresql # Stable PostgreSQL
];
}
Container Generation
Devenv can generate OCI containers from your environment definition, bridging the gap between development and production:
# Build a container image
devenv container build
# Build and load into Docker
devenv container run
Configure the container in devenv.nix:
{ pkgs, ... }:
{
containers.app = {
name = "myapp";
entrypoint = ["python" "-m" "myapp"];
copyToRoot = [
./src
./config
];
};
}
This produces minimal container images because Nix knows exactly which dependencies your application needs -- nothing more.
Testing Environments
Devenv supports defining separate test configurations:
{ pkgs, ... }:
{
env.DATABASE_URL = "postgresql://localhost:5432/myapp_dev";
# Override for testing
enterTest = ''
export DATABASE_URL="postgresql://localhost:5432/myapp_test"
echo "Running in test mode"
'';
}
Run tests with the test environment:
devenv test
Direnv Integration
For the best developer experience, integrate Devenv with direnv so that your environment activates automatically when you cd into the project directory:
# Install direnv
# (on macOS: brew install direnv)
# (on Linux: your package manager)
# Create .envrc in your project root
echo "use devenv" > .envrc
# Allow direnv to load it
direnv allow
Now every time you enter the project directory, your shell automatically has the correct tools, environment variables, and PATH entries. When you leave, they disappear. No manual devenv shell required.
Real-World Configuration: Full-Stack Web Application
Here is a complete devenv.nix for a typical full-stack application with a Python backend, React frontend, PostgreSQL database, and Redis cache:
{ pkgs, ... }:
{
# Languages
languages.python = {
enable = true;
version = "3.12";
venv.enable = true;
venv.requirements = ./requirements.txt;
};
languages.javascript = {
enable = true;
package = pkgs.nodejs_22;
};
# System packages
packages = [
pkgs.pnpm
pkgs.ruff
pkgs.just # Task runner
pkgs.httpie # API testing
];
# Environment variables
env.DJANGO_SETTINGS_MODULE = "config.settings.development";
env.SECRET_KEY = "dev-only-not-for-production";
# Services
services.postgres = {
enable = true;
package = pkgs.postgresql_16;
initialDatabases = [
{ name = "webapp_dev"; }
{ name = "webapp_test"; }
];
};
services.redis.enable = true;
# Processes
processes = {
backend.exec = "python manage.py runserver 0.0.0.0:8000";
frontend.exec = "cd frontend && pnpm dev";
worker.exec = "celery -A config worker -l INFO";
};
# Pre-commit hooks
pre-commit.hooks = {
ruff.enable = true;
prettier.enable = true;
detect-private-key.enable = true;
};
# Scripts
scripts = {
setup.exec = ''
pnpm install --prefix frontend
python manage.py migrate
echo "Setup complete. Run 'devenv up' to start all services."
'';
db-seed.exec = "python manage.py loaddata fixtures/*.json";
};
# Shell hook
enterShell = ''
echo ""
echo "Webapp Development Environment"
echo " Python: $(python --version)"
echo " Node: $(node --version)"
echo " PostgreSQL: $(postgres --version)"
echo ""
echo "Commands: setup, db-seed, devenv up"
'';
}
A new developer joining the team runs three commands:
git clone [email protected]:your-org/webapp.git
cd webapp
devenv shell
setup
devenv up
Five minutes later, they have a fully running application with database, cache, backend, frontend, and worker -- all at the correct versions, all isolated from their global system.
When to Choose Devenv
Devenv is the right choice when your team struggles with "works on my machine" problems, when onboarding a new developer takes more than thirty minutes, when you need reproducible environments across macOS and Linux, or when your project depends on system-level tools (databases, compilers, native libraries) beyond what a single language's package manager provides.
If your project is a single-language application with no system dependencies, a version manager might be enough. If your team is already deep in the Nix ecosystem, raw flakes give you more control. If you need Windows support, Docker dev containers are currently the better option. But for the majority of teams building multi-component applications on macOS and Linux, Devenv offers the best combination of power and usability available today.