devenv
devenv is a fast, declarative tool for creating development environments with Nix. It provides a higher-level interface than raw mkShell and includes built-in support for common development workflows, services, and language ecosystems.
Value proposition
Developer-focused abstractions
While mkShell is powerful, it requires understanding Nix language details and manually configuring common development tools. devenv provides pre-configured modules for languages, databases, and services:
# Traditional mkShell approach
{ pkgs }:
pkgs.mkShell {
packages = with pkgs; [
nodejs
postgresql
redis
];
shellHook = ''
export DATABASE_URL="postgresql://localhost/mydb"
# Start postgres manually...
# Start redis manually...
'';
}
# devenv approach
{
languages.javascript = {
enable = true;
package = pkgs.nodejs;
};
services.postgres = {
enable = true;
initialDatabases = [{ name = "mydb"; }];
};
services.redis.enable = true;
}
devenv handles service lifecycle, environment variables, and common configuration patterns automatically.
Built-in service management
Development often requires running background services like databases, message queues, or cache servers. devenv includes process management through process-compose, allowing you to start all services with a single command:
devenv up
This starts all configured services in the foreground with proper logging and health checks, similar to docker-compose but with Nix’s reproducibility guarantees.
Fast iteration
devenv is optimized for quick feedback loops. It caches evaluation aggressively and provides fast commands for common operations:
devenv shell- Enter the development environmentdevenv test- Run tests defined in your configurationdevenv update- Update dependenciesdevenv info- Show environment information
Language ecosystem integration
devenv understands language-specific conventions and tooling. Instead of manually configuring build tools, package managers, and version managers, you enable a language module:
{
languages.python = {
enable = true;
version = "3.11";
venv.enable = true; # Automatically create and manage virtualenv
venv.requirements = ./requirements.txt;
};
}
This sets up Python, creates a virtualenv, installs dependencies, and configures the environment—all declaratively.
Difference from direnv
While both tools enhance development environments, they serve different purposes and can be used together.
direnv: Environment loader
direnv is a shell extension that automatically loads environment variables and activates development shells when you cd into a directory. It’s:
- Lightweight: Focuses on environment activation
- Shell-agnostic: Works with any shell (bash, zsh, fish)
- Fast: Caches environments for quick activation
- Passive: Only loads environments; doesn’t manage services
devenv: Development environment manager
devenv is a complete development environment toolkit. It:
- Manages services: Starts databases, web servers, background workers
- Configures languages: Sets up language-specific tooling
- Runs processes: Built-in process manager for multi-service development
- Provides workflows: Commands for testing, building, updating
Using them together
devenv and direnv complement each other. Use devenv to define your environment and services, then use direnv to automatically activate it:
# devenv.nix
{
languages.javascript.enable = true;
services.postgres.enable = true;
}
# .envrc
use devenv
Now cd-ing into the directory automatically activates the devenv environment through direnv.
Unique features of devenv
Service lifecycle management
devenv includes process-compose for managing service lifecycles. Define services in your configuration:
{
services.postgres = {
enable = true;
listen_addresses = "127.0.0.1";
port = 5432;
};
services.redis = {
enable = true;
port = 6379;
};
processes = {
web-server = {
exec = "npm run dev";
};
worker = {
exec = "npm run worker";
};
};
}
Start everything with devenv up. Services start in dependency order with proper health checks.
Pre-commit hooks integration
devenv integrates with pre-commit hooks out of the box:
{
pre-commit.hooks = {
nixpkgs-fmt.enable = true;
prettier.enable = true;
eslint.enable = true;
};
}
Hooks install automatically when entering the environment and run before commits.
Container generation
Generate OCI containers from your devenv configuration:
devenv container
This creates a container image with your entire development environment, useful for CI/CD or sharing environments with teammates who prefer containers.
Scripts and tasks
Define project-specific scripts in your configuration:
{
scripts = {
setup.exec = ''
echo "Setting up project..."
npm install
devenv up -d
npm run migrate
'';
test.exec = ''
npm run test
'';
deploy.exec = ''
echo "Deploying..."
npm run build
# deployment logic
'';
};
}
Run them with devenv run setup, devenv run test, etc.
Environment info and debugging
devenv provides introspection commands:
# Show all environment variables
devenv info
# Show service status
devenv status
# Generate shell completion
devenv shell --print-dev-env
These help debug environment issues and understand what’s configured.
Common example
Full-stack web application
A typical web application with Node.js, PostgreSQL, and Redis:
{ pkgs, ... }:
{
# Language configuration
languages = {
javascript = {
enable = true;
package = pkgs.nodejs;
};
};
# Services
services = {
postgres = {
enable = true;
initialDatabases = [{ name = "myapp_dev"; }];
initialScript = ''
CREATE USER myapp WITH PASSWORD 'dev';
GRANT ALL PRIVILEGES ON DATABASE myapp_dev TO myapp;
'';
};
redis = {
enable = true;
};
};
# Processes
processes = {
web = {
exec = "npm run dev";
};
worker = {
exec = "npm run worker";
};
};
# Environment variables
env = {
DATABASE_URL = "postgresql://myapp:dev@localhost/myapp_dev";
REDIS_URL = "redis://localhost:6379";
NODE_ENV = "development";
};
# Development packages
packages = with pkgs; [
postgresql # For psql client
redis # For redis-cli
];
# Scripts
scripts = {
setup.exec = ''
npm install
npm run migrate
'';
reset-db.exec = ''
dropdb --if-exists myapp_dev
createdb myapp_dev
npm run migrate
'';
};
# Pre-commit hooks
pre-commit.hooks = {
prettier.enable = true;
eslint.enable = true;
};
# Enter shell message
enterShell = ''
echo "🚀 Development environment ready!"
echo "Run 'devenv up' to start all services"
echo "Run 'devenv run setup' to initialize the project"
'';
}
Workflow:
# First time setup
devenv shell
devenv run setup
# Daily development
devenv up # Starts postgres, redis, web server, and worker
# In another terminal
devenv shell
npm run test
# Reset database
devenv run reset-db
Python data science environment
A data science project with Python, Jupyter, and PostgreSQL:
{ pkgs, ... }:
{
languages.python = {
enable = true;
version = "3.11";
venv = {
enable = true;
requirements = ''
jupyter
pandas
numpy
matplotlib
psycopg2-binary
sqlalchemy
'';
};
};
services.postgres = {
enable = true;
initialDatabases = [{ name = "data_analysis"; }];
};
processes.jupyter = {
exec = "jupyter lab --ip=0.0.0.0 --port=8888";
};
env = {
DATABASE_URL = "postgresql://localhost/data_analysis";
};
packages = with pkgs; [
postgresql # psql client
];
scripts = {
notebook.exec = "jupyter lab";
load-data.exec = ''
python scripts/load_sample_data.py
'';
};
enterShell = ''
echo "📊 Data science environment ready"
echo "Python $(python --version)"
echo "Run 'devenv up' to start Jupyter and PostgreSQL"
'';
}
Rust project with database
A Rust application with PostgreSQL for integration tests:
{ pkgs, ... }:
{
languages.rust = {
enable = true;
channel = "stable";
};
services.postgres = {
enable = true;
initialDatabases = [
{ name = "myapp_dev"; }
{ name = "myapp_test"; }
];
};
env = {
DATABASE_URL = "postgresql://localhost/myapp_dev";
TEST_DATABASE_URL = "postgresql://localhost/myapp_test";
};
packages = with pkgs; [
postgresql
sqlx-cli # Database migration tool
];
scripts = {
migrate.exec = "sqlx migrate run";
test.exec = ''
sqlx database reset -y --database-url $TEST_DATABASE_URL
cargo test
'';
dev.exec = "cargo watch -x run";
};
pre-commit.hooks = {
rustfmt.enable = true;
clippy.enable = true;
};
processes = {
api = {
exec = "cargo run";
};
};
enterShell = ''
echo "🦀 Rust development environment"
rustc --version
cargo --version
'';
}
Common issues
Services fail to start
When devenv up fails to start services, check the logs for specific error messages. Services might fail due to:
- Port conflicts with existing processes
- Missing initialization or migration steps
- Incorrect configuration
View detailed service output:
devenv up --verbose
Check if ports are already in use:
lsof -i :5432 # Check PostgreSQL default port
lsof -i :6379 # Check Redis default port
Stop conflicting services or change ports in your devenv configuration:
{
services.postgres.port = 5433; # Use different port
services.redis.port = 6380;
}
Environment not updating after changes
After modifying devenv.nix, the environment might not reflect changes immediately. devenv caches evaluations for performance, so you need to reload:
# Exit and re-enter the shell
exit
devenv shell
# Or reload within the shell
direnv reload # if using direnv
For service changes, restart them:
# Stop services
Ctrl+C # in devenv up terminal
# Restart
devenv up
Python venv issues
When using Python’s venv integration, dependencies might not install correctly or the virtualenv might get corrupted. This often happens after changing requirements.txt or Python version. Regenerate the virtualenv:
# Remove existing venv
rm -rf .devenv
# Re-enter shell to recreate
exit
devenv shell
devenv creates a new virtualenv and installs dependencies fresh.
Slow shell activation
First-time activation can be slow as devenv builds the environment and installs packages. Subsequent activations are much faster due to caching. For very slow environments, check if:
- You’re building packages from source unnecessarily
- Large dependencies are being downloaded
- Many pre-commit hooks are installing
Use binary caches to avoid building from source:
{
# Use cachix for faster package downloads
cachix = {
enable = true;
caches = [ "devenv" ];
};
}
Permission errors with services
Service data directories sometimes have permission issues, especially when switching between different configurations. The .devenv directory stores service data and might have incorrect permissions. Clear the state:
rm -rf .devenv/state
devenv up
Services recreate their data directories with correct permissions.
Pre-commit hooks not running
When pre-commit hooks aren’t executing on commit, they might not be installed. devenv installs hooks automatically, but only within the development shell. Ensure you’re committing from within the devenv shell:
devenv shell
git commit # Hooks run here
Or explicitly install hooks:
devenv shell
pre-commit install
Process crashes immediately
If a process defined in processes crashes immediately when running devenv up, check the command is correct and dependencies are available. Add debugging output to the exec command:
{
processes.web = {
exec = ''
echo "Starting web server..."
set -x # Enable debug output
npm run dev
'';
};
}
View the full output when running devenv up to see what’s failing.
Conflicting package versions
Multiple services or language configurations might pull in conflicting package versions. devenv tries to resolve these, but sometimes manual intervention is needed. Override specific packages:
{ pkgs, ... }:
{
languages.python.enable = true;
# Override a specific package
packages = with pkgs; [
(python311.withPackages (ps: with ps; [
# Specific versions
django_4
psycopg2
]))
];
}
Database initialization failures
When PostgreSQL or other database services fail during initialization, often the initial script has errors or the database already exists. Check initialization logs:
devenv up --verbose
Clear the database state and retry:
rm -rf .devenv/state/postgres
devenv up
The database recreates from scratch with your initialScript or initialDatabases configuration.
Integration patterns
With direnv
Automatically activate devenv when entering the directory:
# .envrc
use devenv
This combines direnv’s automatic activation with devenv’s full environment management.
With CI/CD
Use devenv in CI to get identical environments:
# GitHub Actions example
- uses: cachix/install-nix-action@v22
- name: Install devenv
run: nix-env -iA devenv -f https://install.devenv.sh/latest
- name: Run tests
run: devenv test
Or generate a container:
devenv container
# Use the generated container in CI
With Docker for production
While devenv excels at development, you might still use Docker for production. Generate a devenv container for dev/prod parity:
{
containers.app = {
name = "myapp";
copyToRoot = pkgs.buildEnv {
name = "image-root";
paths = [ pkgs.nodejs ];
};
config = {
Cmd = [ "npm" "start" ];
};
};
}
Build and run:
devenv container
docker load < ./result
docker run myapp
Best practices
Keep devenv.nix focused
Use devenv.nix for environment configuration and keep application logic in standard build files:
# Good: Environment only
{
languages.javascript.enable = true;
services.postgres.enable = true;
}
# Less good: Mixing app logic
{
languages.javascript.enable = true;
scripts.complex-app-build.exec = ''
# 100 lines of application-specific build logic
'';
}
Application builds belong in package.json, Makefile, or similar.
Use scripts for common tasks
Define frequently-used commands as scripts:
{
scripts = {
db-reset.exec = "dropdb myapp && createdb myapp && migrate";
test-watch.exec = "npm test -- --watch";
lint.exec = "npm run lint && cargo clippy";
};
}
This documents common workflows and makes them easily discoverable for team members.
Version control devenv.lock
devenv generates a devenv.lock file pinning all dependencies. Commit this file to ensure reproducibility:
git add devenv.nix devenv.lock
git commit -m "Add devenv configuration"
Document the environment
Use enterShell to show helpful information:
{
enterShell = ''
cat <<EOF
🎯 Project Development Environment
Available commands:
devenv up - Start all services
devenv run setup - Initialize project
devenv run test - Run tests
Services:
PostgreSQL - localhost:5432
Redis - localhost:6379
Documentation: https://github.com/myorg/myproject/wiki/dev-setup
EOF
'';
}
Layer environments carefully
For complex projects with multiple environments (dev, test, staging), use composition:
# devenv.nix (base config)
{ pkgs, ... }:
{
imports = [ ./devenv-base.nix ];
# Development-specific
services.postgres.initialDatabases = [{ name = "myapp_dev"; }];
}
# devenv-test.nix
{ pkgs, ... }:
{
imports = [ ./devenv-base.nix ];
# Test-specific
services.postgres.initialDatabases = [{ name = "myapp_test"; }];
}
Further reading
devenv brings the convenience of tools like docker-compose to Nix development environments while maintaining reproducibility and the full power of nixpkgs.