Use Nix to build reproducible development environments for most programming languages.
Why manage development environments with Nix?#
The original language examples pin nixos-24.05. Python 3.11, Go 1.21, Node 20, and the other versions shown are examples, not recommendations for currently secure versions. Choose supported toolchains and commit flake.lock; also manage the project package manager’s lockfile, external services, and runtime state.
Problems with traditional development environments:
- Conflicting dependencies across projects
- "It works on my machine"
- Time-consuming setup for new team members
- System upgrades breaking the environment
How Nix addresses them:
- Independent dependencies for each project
- Reusable tool environments with pinned inputs
- One-command startup for the development environment
- Isolation from the system environment
The basic approach: nix-shell / nix run#
Temporary environments
# 进入带特定软件包的临时 shell
nix-shell -p python311 git gcc
# 运行单个命令
nix-shell -p nodejs --run "node --version"
# 使用 nix run(Flakes 方式)
nix run nixpkgs#python311 -- -c "print('Hello Nix')"The shell.nix file
Create shell.nix in the project root:
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
name = "my-project-env";
buildInputs = with pkgs; [
# 构建工具
gcc
gnumake
cmake
# 语言工具
python311
nodejs_20
rustc
cargo
# 开发工具
git
vim
htop
strace
gdb
];
# 环境变量
MY_PROJECT_ROOT = builtins.toString ./.;
MY_ENV = "development";
shellHook = ''
echo "======================================"
echo " 欢迎来到 $name"
echo "======================================"
echo ""
# 显示版本
echo "工具版本:"
python3 --version
node --version
rustc --version
cargo --version
echo ""
# 设置提示符
export PS1="\[\033[01;34m\][$name]\[\033[00m\] \w \$ "
'';
}Usage:
cd my-project
nix-shellThe modern approach: Flakes devShells (recommended)#
A basic Flake development environment
# flake.nix
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.${system}.default = pkgs.mkShell {
name = "dev-shell";
packages = with pkgs; [
git
vim
direnv
];
};
};
}Usage:
nix develop # 进入默认 shell
nix develop .#python-dev # 进入特定 shellDevelopment environments by language#
Python
The basic approach
# flake.nix
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.${system}.default = pkgs.mkShell {
name = "python-dev";
packages = with pkgs; [
(python311.withPackages (ps: with ps; [
# Web 框架
flask
django
fastapi uvicorn
# 数据处理
numpy pandas matplotlib
requests beautifulsoup4
# 测试与工具
pytest black mypy pylint
ipython jupyter
# 数据库
sqlalchemy psycopg2
]))
# 其他工具
poetry
pipenv
git
];
shellHook = ''
export PYTHONPATH="$(pwd)/src:$PYTHONPATH"
# withPackages 已提供 Python 依赖,不伪造 VIRTUAL_ENV
# 若要虚拟环境,需另行 python -m venv 并明确 site-packages 策略
echo "Python $(python --version) 环境已加载"
# 创建 alias
alias py='python'
alias pyserver='python -m http.server'
'';
};
};
}Using Poetry
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
python311
poetry
git
];
shellHook = ''
# 配置 Poetry 使用项目本地虚拟环境
export POETRY_VIRTUALENVS_IN_PROJECT=true
export POETRY_NO_INTERACTION=1
# 依赖安装明确执行,不在目录进入时隐式触发网络或写文件
# 确认 poetry.lock 后,手动运行 poetry install
echo "Poetry 环境已准备"
poetry --version
'';
};
};
}Node.js / JavaScript
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.${system}.default = pkgs.mkShell {
name = "nodejs-dev";
packages = with pkgs; [
nodejs_20
corepack # npm, yarn, pnpm
# 包管理器
yarn
pnpm
# 工具
git
nodePackages.typescript
nodePackages.ts-node
nodePackages.nodemon
nodePackages.eslint
nodePackages.prettier
];
shellHook = ''
export PATH="$(pwd)/node_modules/.bin:$PATH"
# 项目安装单独执行;有 package-lock.json 时可用 npm ci
# npm install 会写入项目依赖并可能更新 lock,不在 shellHook 自动执行
echo "Node.js $(node --version)"
echo "npm $(npm --version)"
'';
};
};
}Rust
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = { self, nixpkgs, rust-overlay }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ rust-overlay.overlays.default ];
};
in
{
devShells.${system}.default = pkgs.mkShell {
name = "rust-dev";
packages = with pkgs; [
# Rust 工具链
(rust-bin.stable.latest.default.override {
extensions = [ "rust-src" "rustfmt" "clippy" ];
targets = [ "x86_64-unknown-linux-gnu" "wasm32-unknown-unknown" ];
})
# 构建工具
cargo-watch
cargo-edit
cargo-audit
cargo-tarpaulin # 覆盖率
cargo-flamegraph # 性能分析
# 系统依赖
openssl
pkg-config
gcc
# IDE 支持
rust-analyzer
lldb
];
# 环境变量
RUST_BACKTRACE = 1;
RUST_LOG = "debug";
shellHook = ''
echo "Rust $(rustc --version)"
echo "Cargo $(cargo --version)"
# 有用的 alias
alias cw='cargo watch -x run'
alias ct='cargo test'
alias cf='cargo fmt'
alias ccl='cargo clippy'
'';
};
};
}Go
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.${system}.default = pkgs.mkShell {
name = "go-dev";
packages = with pkgs; [
go_1_21
golangci-lint
gopls # LSP
gotools # goimports, gorename, etc
delve # 调试器
git
postgresql # 如果项目用到了
];
shellHook = ''
# Nix 字符串不展开 $HOME,交给 shell 设置
export GOPATH="$HOME/go"
export PATH="$GOPATH/bin:$PATH"
echo "Go $(go version)"
echo "GOPATH: $GOPATH"
'';
};
};
}Java
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.${system}.default = pkgs.mkShell {
name = "java-dev";
packages = with pkgs; [
jdk21
maven
gradle
# 工具
git
jflex
# IDE 支持
jdt-language-server
];
shellHook = ''
export JAVA_HOME=${pkgs.jdk21}
echo "Java $(java --version | head -1)"
echo "Maven $(mvn --version | head -1)"
'';
};
};
}C/C++
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.${system}.default = pkgs.mkShell {
name = "cpp-dev";
packages = with pkgs; [
# 编译器
gcc13
clang_16
llvmPackages.clang-tools # clangd, clang-format
# 构建工具
cmake
ninja
gnumake
pkg-config
# 调试与分析
gdb
lldb
valgrind
cppcheck
# 库
boost
catch2
fmt
spdlog
# 其他
git
cmake-language-server
];
shellHook = ''
export CC=gcc
export CXX=g++
echo "GCC $(gcc --version | head -1)"
echo "Clang $(clang --version | head -1)"
'';
};
};
}Haskell
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
haskellPackages = pkgs.haskellPackages.override {
overrides = hself: hsuper: {
# 可以覆盖特定包版本
};
};
in
{
devShells.${system}.default = haskellPackages.shellFor {
packages = p: [
# 你的项目包
];
withHoogle = true;
buildInputs = with haskellPackages; [
ghc
cabal-install
stack
haskell-language-server
hlint
stylish-haskell
pkgs.zlib
pkgs.git
];
};
};
}Automatic loading with Direnv#
Installation and configuration
direnv can automatically load a development environment when you enter a directory.
# /etc/nixos/configuration.nix 或 flake.nix
{ pkgs, ... }: {
programs.direnv = {
enable = true;
nix-direnv.enable = true; # 更快的加载
};
}Project configuration
use flake and use nix are alternatives; do not call both for the same environment. Read .envrc and shellHook before authorizing them, because they execute code.
Create .envrc in the project root:
# 使用 flake
use flake
# 或使用 shell.nix(传统方式)
use nixThen authorize it:
direnv allowThe environment now loads automatically when you enter the directory and unloads when you leave.
Database services#
This kind of shellHook creates directories and starts background processes; exiting the devShell does not automatically stop the services. Add data directories to .gitignore and pin database major versions to avoid incompatibility with existing data directories. Use passwordless authentication only for local development.
PostgreSQL
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
postgresql
pgcli
redis
];
shellHook = ''
# 设置临时 PostgreSQL 目录
export PGDATA="$PWD/.postgres"
export PGHOST="$PGDATA"
export PGDATABASE="devdb"
if [ ! -d "$PGDATA" ]; then
echo "初始化 PostgreSQL..."
initdb --auth-local=trust --auth-host=scram-sha-256 --no-locale --encoding=UTF8
# 启动 PostgreSQL
pg_ctl start -l "$PGDATA/log" -o "-k $PGDATA -c listen_addresses="
sleep 2
# 创建数据库
createdb devdb
echo "数据库已创建: $PGDATABASE"
else
pg_ctl start -l "$PGDATA/log" -o "-k $PGDATA -c listen_addresses="
fi
pgstart() { pg_ctl start -o "-k $PGDATA -c listen_addresses="; }
alias pgstop='pg_ctl stop'
alias pgstatus='pg_ctl status'
'';
};
};
}Redis
{
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
redis
];
shellHook = ''
export REDIS_DATA="$PWD/.redis"
mkdir -p "$REDIS_DATA"
# 配置文件
cat > $REDIS_DATA/redis.conf << EOF
bind 127.0.0.1
protected-mode yes
port 6379
dir $REDIS_DATA
daemonize yes
pidfile $REDIS_DATA/redis.pid
logfile $REDIS_DATA/redis.log
EOF
if ! redis-cli ping > /dev/null 2>&1; then
echo "启动 Redis..."
redis-server $REDIS_DATA/redis.conf
fi
alias rdc='redis-cli'
'';
};
}Container development#
This provides the Docker CLI; it does not automatically configure or start the host Docker daemon or grant access to its socket. Declare container services separately.
Docker Compose
{
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
docker
docker-compose
docker-buildx
];
shellHook = ''
# 检查 Docker 服务
if ! docker info > /dev/null 2>&1; then
echo "警告: Docker 服务未运行"
fi
alias dc='docker-compose'
alias dcb='docker-compose build'
alias dcup='docker-compose up -d'
alias dcdown='docker-compose down'
'';
};
}A complete project example#
A full-stack web project
# flake.nix
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.${system}.default = pkgs.mkShell {
name = "web-fullstack";
packages = with pkgs; [
# 前端
nodejs_20
yarn
nodePackages.typescript
nodePackages.vite
# 后端 (Python)
(python311.withPackages (ps: with ps; [
fastapi
uvicorn
sqlalchemy
alembic
pytest
black
mypy
]))
poetry
# 数据库
postgresql
redis
pgcli
# 工具
git
just
jq
httpie
# IDE 支持
nodePackages.typescript-language-server
nodePackages.vscode-langservers-extracted
pyright
];
shellHook = ''
export PATH="$(pwd)/frontend/node_modules/.bin:$(pwd)/backend/.venv/bin:$PATH"
export PGDATA="$PWD/.postgres"
echo ""
echo "========================================"
echo " Web Full-Stack Development Environment"
echo "========================================"
echo ""
echo "可用命令:"
echo " just dev - 启动所有服务"
echo " just frontend - 仅启动前端"
echo " just backend - 仅启动后端"
echo " just db - 管理数据库"
echo ""
'';
};
};
}Justfile (command runner)
The recipes require the project’s npm scripts, Poetry configuration, and Alembic migrations to exist already. dev is an illustrative script for starting services concurrently; the project must handle cleanup on exit and port conflicts. Initialize PostgreSQL first, then start it, then create the database.
# justfile
set dotenv-load
# 开发命令
dev:
just frontend &
just backend &
wait
frontend:
cd frontend && npm run dev
backend:
cd backend && poetry run uvicorn main:app --reload
db-start:
pg_ctl start -l "$PGDATA/log" -o "-k $PGDATA -c listen_addresses="
db-stop:
pg_ctl stop
db-init:
initdb --auth-local=trust --auth-host=scram-sha-256 --no-locale --encoding=UTF8
pg_ctl start -l "$PGDATA/log" -o "-k $PGDATA -c listen_addresses="
PGHOST="$PGDATA" createdb devdb
db-migrate:
cd backend && poetry run alembic upgrade head
# 前端命令
fe-build:
cd frontend && npm run build
fe-test:
cd frontend && npm test
fe-lint:
cd frontend && npm run lint
# 后端命令
be-test:
cd backend && poetry run pytest
be-lint:
cd backend && poetry run black .
cd backend && poetry run mypy .
be-format:
cd backend && poetry run black .Best practices#
- Use Flakes: pin Nix inputs; external downloads and mutable state still need to be handled
- Pair with direnv: enable automatic loading
- Document the setup: explain how to start the environment in the README
- Pin versions: commit flake.lock
- Support multiple platforms: support multiple systems where possible
Example completeness and real projects
The complete Flake example includes inputs, outputs, system, and pkgs; the Redis and Docker snippets omit the surrounding structure and must be placed inside an existing outputs scope. The empty packages list in haskellPackages.shellFor is only a skeleton: fill it with the project’s derivation. corepack manages Yarn/pnpm shims and does not provide the npm bundled with Node; check the pinned Nixpkgs to see whether the package names still exist. The database examples avoid passwordless TCP listeners; these services and development environments have not been run locally.
References & further reading
Adapted from technical notes. Examples use generic names.