Develop Workflow
Create and use Nix development environments with direnv integration.
When to Use
- “nix development shell”
- “nix develop”
- “development environment”
- “direnv with nix”
Quick Commands
Development Shells
# Enter development shell
nix develop
# Enter shell for specific package
nix develop .#package-name
# Run command in shell
nix develop -c make build
nix develop --command npm test
# Print shell environment
nix develop --print-build-logs
direnv Integration
# Create .envrc
echo "use flake" > .envrc
# Allow direnv
direnv allow
# Reload environment
direnv reload
# Check status
direnv status
Creating Development Shells
Basic devShell
# flake.nix
{
outputs = { self, nixpkgs }: {
devShells.x86_64-linux.default = let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in pkgs.mkShell {
buildInputs = with pkgs; [
go
gopls
golangci-lint
];
shellHook = ''
echo "Welcome to dev environment"
go version
'';
};
};
}
Multi-Shell Setup
# Multiple development shells
{
outputs = { self, nixpkgs }: {
devShells.x86_64-linux = {
default = pkgs.mkShell {
buildInputs = [ pkgs.go ];
};
rust = pkgs.mkShell {
buildInputs = with pkgs; [
rustc
cargo
rust-analyzer
];
};
python = pkgs.mkShell {
buildInputs = with pkgs; [
python3
python3Packages.pip
];
};
};
};
}
Access Specific Shell
# Use default shell
nix develop
# Use named shell
nix develop .#rust
nix develop .#python
Shell Configuration
Environment Variables
pkgs.mkShell {
buildInputs = [ pkgs.go ];
shellHook = ''
export GOPATH=$PWD/.go
export GOCACHE=$PWD/.cache/go-build
export PATH=$GOPATH/bin:$PATH
# Create directories
mkdir -p $GOPATH $GOCACHE
'';
# Or use env
env = {
GOPATH = "$PWD/.go";
GOCACHE = "$PWD/.cache/go-build";
};
}
Shell Hooks
pkgs.mkShell {
buildInputs = [ pkgs.nodejs ];
shellHook = ''
# Print banner
echo "================================"
echo "Node.js Development Environment"
echo "================================"
node --version
npm --version
# Install dependencies
if [ ! -d "node_modules" ]; then
npm install
fi
# Setup git hooks
if [ ! -f ".git/hooks/pre-commit" ]; then
echo "Setting up git hooks..."
npx husky install
fi
'';
}
Package-Specific Shell
# For a specific package's dev environment
{
outputs = { self, nixpkgs }: {
packages.x86_64-linux.myapp = /* package definition */;
devShells.x86_64-linux.default = let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in pkgs.mkShell {
# Include all build inputs from package
inputsFrom = [ self.packages.x86_64-linux.myapp ];
# Add development tools
buildInputs = with pkgs; [
go-tools
delve
gopls
];
};
};
}
direnv Integration
Setup direnv
# Install direnv
nix profile install nixpkgs#direnv
nix profile install nixpkgs#nix-direnv
# Configure shell (bash)
eval "$(direnv hook bash)"
# Configure shell (zsh)
eval "$(direnv hook zsh)"
Basic .envrc
# .envrc
use flake
# Optional: Use specific flake output
# use flake .#rust
Advanced .envrc
# .envrc
use flake
# Watch additional files
watch_file shell.nix
watch_file flake.nix
watch_file flake.lock
# Set environment variables
export EDITOR=vim
export DATABASE_URL=postgresql://localhost/mydb
# Source local secrets
if [ -f .env.local ]; then
dotenv .env.local
fi
# Layout for different languages
# layout python # Auto-create venv
# layout node # Add node_modules/.bin to PATH
nix-direnv Configuration
# ~/.config/direnv/direnvrc or ~/.direnvrc
source $HOME/.nix-profile/share/nix-direnv/direnvrc
# Or if installed via home-manager
# It's automatically configured
Performance with nix-direnv
# nix-direnv caches the environment
# Much faster reloads!
# First load (slow)
$ cd project/
direnv: loading ~/project/.envrc
direnv: using flake
# ... downloads and builds ...
# Subsequent loads (instant!)
$ cd project/
direnv: loading ~/project/.envrc
direnv: using cached flake environment
Common Development Patterns
Go Development
{
devShells.x86_64-linux.default = pkgs.mkShell {
buildInputs = with pkgs; [
go
gopls
gotools
golangci-lint
delve
];
shellHook = ''
export GOPATH=$PWD/.go
export GOCACHE=$PWD/.cache/go-build
export PATH=$GOPATH/bin:$PATH
mkdir -p $GOPATH $GOCACHE
'';
};
}
Rust Development
{
devShells.x86_64-linux.default = pkgs.mkShell {
buildInputs = with pkgs; [
rustc
cargo
rustfmt
rust-analyzer
clippy
];
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
};
}
Node.js Development
{
devShells.x86_64-linux.default = pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_20
nodePackages.npm
nodePackages.typescript
nodePackages.prettier
];
shellHook = ''
export NODE_PATH=$PWD/node_modules
export PATH=$PWD/node_modules/.bin:$PATH
'';
};
}
Python Development
{
devShells.x86_64-linux.default = let
python = pkgs.python3.withPackages (ps: with ps; [
flask
requests
pytest
]);
in pkgs.mkShell {
buildInputs = [ python ];
shellHook = ''
# Create virtual environment if needed
if [ ! -d "venv" ]; then
python -m venv venv
fi
source venv/bin/activate
'';
};
}
C/C++ Development
{
devShells.x86_64-linux.default = pkgs.mkShell {
buildInputs = with pkgs; [
gcc
cmake
gnumake
gdb
clang-tools
];
shellHook = ''
export CC=gcc
export CXX=g++
'';
};
}
Advanced Shell Features
Multiple Package Sets
{
devShells.x86_64-linux.default = let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
unstable = inputs.nixpkgs-unstable.legacyPackages.x86_64-linux;
in pkgs.mkShell {
buildInputs = [
pkgs.go # Stable Go
unstable.gopls # Latest gopls
];
};
}
Conditional Tools
pkgs.mkShell {
buildInputs = with pkgs; [
go
gopls
] ++ lib.optionals stdenv.isLinux [
# Linux-specific tools
strace
ltrace
] ++ lib.optionals stdenv.isDarwin [
# macOS-specific tools
darwin.apple_sdk.frameworks.Security
];
}
Project-Specific Scripts
pkgs.mkShell {
buildInputs = [ pkgs.go ];
shellHook = ''
# Create helper scripts
cat > run.sh <<'EOF'
#!/usr/bin/env bash
go run ./cmd/server
EOF
chmod +x run.sh
cat > test.sh <<'EOF'
#!/usr/bin/env bash
go test ./... -v
EOF
chmod +x test.sh
echo "Helper scripts created: run.sh, test.sh"
'';
}
Development Workflow
Typical Session
# Enter project
cd myproject/
# direnv automatically loads environment
direnv: loading ~/myproject/.envrc
direnv: using flake
# Environment ready
go version
gopls version
# Work on project
vim main.go
go build
go test
# Leave project
cd ..
# Environment unloaded
direnv: unloading
Update Development Environment
# Update flake inputs
nix flake update
# Reload direnv
direnv reload
# Or manually re-enter
nix develop
Share Environment
# Commit flake.nix and flake.lock
git add flake.nix flake.lock
git commit -m "chore: add nix development environment"
# Team members get same environment
git clone repo
cd repo
direnv allow # Or nix develop
Troubleshooting
direnv Not Loading
# Check direnv status
direnv status
# Manually allow
direnv allow
# Check .envrc syntax
cat .envrc
# Test flake
nix flake check
Slow Shell Activation
# Install nix-direnv for caching
nix profile install nixpkgs#nix-direnv
# Configure in ~/.config/direnv/direnvrc
source $HOME/.nix-profile/share/nix-direnv/direnvrc
# Reload
direnv reload
Environment Variables Not Set
# Check environment
nix develop --command printenv
# Verify shellHook
nix develop --command bash -c 'echo $MY_VAR'
# Debug direnv
direnv exec . env | grep MY_VAR
Binary Cache Issues
# Use substituters
nix develop --extra-substituters https://cache.nixos.org
# Build locally if needed
nix develop --no-substitute
Best Practices
- Use direnv: Automatic environment loading
- Install nix-direnv: Much faster reloads with caching
- Commit flake.lock: Ensure reproducibility
- Pin versions: Use specific nixpkgs inputs
- Minimal dependencies: Only what’s needed for development
- Document setup: Add README with instructions
- Use inputsFrom: Inherit from package definitions
- Test environment: Verify with fresh clone
Integration with Editors
VS Code
// .vscode/settings.json
{
"nix.enableLanguageServer": true,
"nix.serverPath": "nixd"
}
# Install nixd language server in devShell
buildInputs = [ pkgs.nixd ];
Vim/Neovim
# Install LSPs in devShell
buildInputs = with pkgs; [
gopls # Go
rust-analyzer # Rust
nodePackages.typescript-language-server # TypeScript
];
Emacs
;; direnv integration
(use-package direnv
:config
(direnv-mode))
Common Patterns
Database Development
pkgs.mkShell {
buildInputs = with pkgs; [
postgresql
pgcli
];
shellHook = ''
export PGDATA=$PWD/.postgres
export PGHOST=$PWD/.postgres
export PGDATABASE=mydb
if [ ! -d "$PGDATA" ]; then
initdb --no-locale --encoding=UTF8
echo "unix_socket_directories = '$PGHOST'" >> $PGDATA/postgresql.conf
pg_ctl start -l $PGDATA/logfile
createdb $PGDATABASE
else
pg_ctl start -l $PGDATA/logfile
fi
'';
}
Docker Development
pkgs.mkShell {
buildInputs = with pkgs; [
docker
docker-compose
];
shellHook = ''
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
'';
}
Multiple Environments
# .envrc
# Choose environment based on directory
if [ -f "Cargo.toml" ]; then
use flake .#rust
elif [ -f "go.mod" ]; then
use flake .#go
elif [ -f "package.json" ]; then
use flake .#node
else
use flake
fi