| name | nix-reproducible-builds |
| description | Use Nix and NixOS for fully reproducible development environments, CI pipelines, and deployments. Covers flake-based project setup, devShells, Nix overlays, home-manager, NixOS configuration, and building Docker images with Nix for bit-reproducible containers. |
| version | 1.0.0 |
| tags | ["nix","nixos","flakes","devshell","reproducible-builds","home-manager","nix-overlays","docker-nix","ci-cd"] |
Nix Reproducible Builds
Overview
Nix is a purely functional package manager where every package is built in isolation, pinned by cryptographic hash, and stored immutably — making builds reproducible across any Linux or macOS machine, any CI runner, and any point in time. Nix Flakes formalize this with locked dependency graphs (flake.lock), replacing shell scripts and Docker snapshots as the reproducibility mechanism. The result: "works on my machine" becomes a guarantee, not a hope.
When to Use
- Teams where "it works on my machine" is a recurring problem across dev/CI/production
- Projects needing exact binary reproducibility for compliance or security audit trails
- Multi-language monorepos where each language needs different toolchain versions simultaneously
- CI pipelines that must be fast, cache-efficient, and deterministic
- Replacing Dockerfile-based environments with something that's both hermetic and composable
- NixOS servers that need declarative, rollback-capable system configuration
- Building minimal Docker images without the overhead of layered Dockerfiles
Step-by-Step Workflow
1. Install Nix and Enable Flakes
sh <(curl -L https://nixos.org/nix/install) --daemon
curl --proto '=https' --tlsv1.2 -sSf https://install.determinate.systems/nix | sh -s -- install
echo 'experimental-features = nix-command flakes' | sudo tee -a /etc/nix/nix.conf
nix --version
nix flake --help
2. Basic flake.nix — Project Dev Shell
# flake.nix — pin all tools for your project
{
description = "My project development environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
in
{
# Development shell: `nix develop` or `direnv allow`
devShells.default = pkgs.mkShell {
packages = with pkgs; [
# Language runtimes — pinned versions
nodejs_22
python312
go_1_22
rustup
# Build tools
gnumake
cmake
# CLI utilities
jq
curl
git
gh
# Database tools
postgresql_16
redis
];
# Environment variables set in the shell
shellHook = ''
echo "Dev environment loaded!"
echo "Node: $(node --version)"
echo "Python: $(python --version)"
export DATABASE_URL="postgresql://localhost:5432/mydb"
export REDIS_URL="redis://localhost:6379"
'';
};
# Expose packages defined in this flake
packages.default = pkgs.stdenv.mkDerivation {
name = "my-app";
src = ./.;
buildInputs = with pkgs; [ nodejs_22 ];
buildPhase = "npm ci && npm run build";
installPhase = "cp -r dist $out";
};
}
);
}
nix develop
nix develop --command node index.js
nix flake update
nix flake lock
nix flake show
nix flake metadata
echo "use flake" > .envrc
direnv allow
3. Multi-Language Project with Overlays
# flake.nix — override specific package versions with overlays
{
description = "Project with custom package versions";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};
outputs = { self, nixpkgs, nixpkgs-unstable }:
let
system = "x86_64-linux";
# Overlay: cherry-pick packages from unstable into stable
overlay = final: prev: {
# Use unstable's Node.js
nodejs = nixpkgs-unstable.legacyPackages.${system}.nodejs_22;
# Custom package: install a specific npm package globally
my-cli = prev.nodePackages.buildNodeApplication {
name = "my-cli";
src = ./cli;
version = "1.0.0";
};
};
pkgs = import nixpkgs {
inherit system;
overlays = [ overlay ];
};
in
{
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
nodejs # Now uses unstable's version via overlay
python312
poetry
my-cli
];
};
# Multiple shells for different purposes
devShells.${system}.backend = pkgs.mkShell {
packages = with pkgs; [ go_1_22 golangci-lint protobuf grpc ];
};
devShells.${system}.frontend = pkgs.mkShell {
packages = with pkgs; [ nodejs_22 yarn chromium playwright-driver.browsers ];
shellHook = ''
export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers}
'';
};
};
}
4. Building Docker Images with Nix
# Build a minimal, reproducible Docker image — no Dockerfile needed
# The resulting image has no shell, no package manager, only what's declared
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
# Build: nix build .#dockerImage
# Load: docker load < result
packages.${system}.dockerImage = pkgs.dockerTools.buildLayeredImage {
name = "my-app";
tag = "latest";
# Contents: only what you declare
contents = with pkgs; [
# The app binary
(pkgs.buildGoModule {
pname = "my-server";
version = "1.0.0";
src = ./server;
vendorHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
})
# Minimal runtime deps
cacert # TLS certificates
tzdata # Timezone data
];
config = {
Cmd = [ "/bin/my-server" ];
ExposedPorts = { "8080/tcp" = {}; };
Env = [
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
"TZDIR=${pkgs.tzdata}/share/zoneinfo"
];
# Run as non-root
User = "1000:1000";
};
# Maximize layer sharing across images
maxLayers = 120;
};
# Scratch image (zero packages, just your binary)
packages.${system}.minimalImage = pkgs.dockerTools.buildImage {
name = "my-app-minimal";
tag = "scratch";
copyToRoot = pkgs.buildEnv {
name = "image-root";
paths = [ self.packages.${system}.myBinary ];
pathsToLink = [ "/bin" ];
};
config.Cmd = [ "/bin/my-server" ];
};
};
}
nix build .#dockerImage
docker load < result
docker tag my-app:latest registry.example.com/my-app:$(git rev-parse --short HEAD)
docker push registry.example.com/my-app:$(git rev-parse --short HEAD)
docker inspect my-app:latest | jq '.[0].RootFS.Layers | length'
5. NixOS System Configuration
# /etc/nixos/configuration.nix — declarative server setup
{ config, pkgs, ... }:
{
# System basics
system.stateVersion = "24.05";
networking.hostName = "my-server";
time.timeZone = "America/Chicago";
# User accounts
users.users.deploy = {
isNormalUser = true;
extraGroups = [ "docker" "sudo" ];
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3... deploy@ci"
];
};
# Services
services.nginx = {
enable = true;
virtualHosts."app.example.com" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:3000";
proxyWebsockets = true;
extraConfig = "proxy_read_timeout 60s;";
};
};
};
services.postgresql = {
enable = true;
package = pkgs.postgresql_16;
settings = {
max_connections = 200;
shared_buffers = "256MB";
work_mem = "4MB";
};
ensureDatabases = [ "myapp" ];
ensureUsers = [{
name = "myapp";
ensureDBOwnership = true;
}];
};
services.redis.servers."".enable = true;
# Docker
virtualisation.docker.enable = true;
virtualisation.docker.autoPrune.enable = true;
# Firewall
networking.firewall = {
enable = true;
allowedTCPPorts = [ 22 80 443 ];
};
# Auto-upgrade
system.autoUpgrade = {
enable = true;
allowReboot = false;
flake = "github:myorg/nixos-config#my-server";
dates = "04:00";
};
}
sudo nixos-rebuild switch
sudo nixos-rebuild switch --rollback
nix-env --list-generations --profile /nix/var/nix/profiles/system
sudo nixos-rebuild test
sudo nixos-rebuild dry-activate
6. CI Pipeline with Nix
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
- name: Setup Nix Cache (Magic Nix Cache)
uses: DeterminateSystems/magic-nix-cache-action@main
- name: Run tests
run: |
# All tools come from the flake — no apt-get, no brew, no setup actions
nix develop --command bash -c "
npm ci
npm run test
npm run build
"
- name: Build Docker image
run: |
nix build .#dockerImage
docker load < result
- name: Security scan
run: |
# Nix's dependency graph is fully auditable
nix build .#dockerImage
# Generate SBOM from Nix store paths
nix-store --query --requisites result | sort > sbom.txt
nix-env -iA nixpkgs.cachix
cachix use my-project
cachix authtoken <token>
nix build .#dockerImage | cachix push my-project
Key Commands Reference
nix develop
nix develop --command CMD
nix build .#package
nix run .#app -- --args
nix shell nixpkgs#curl
nix flake update
nix flake check
nix flake show
nix search nixpkgs nodejs
nix-env -qaP | grep python
nix-collect-garbage -d
nix store gc
nix path-info .#dockerImage --recursive
nix show-derivation .#myPackage
nix why-depends .#app nixpkgs#glibc
nix profile install nixpkgs#htop
nix profile list
nix profile remove htop
sudo nixos-rebuild switch
sudo nixos-rebuild switch --rollback
nixos-option services.nginx.enable
echo "use flake" > .envrc && direnv allow
Common Patterns
Pattern 1: Pinning a Specific Node.js Version for a Project
# Useful when different projects need different Node versions
# Project A: Node 18 (legacy app)
# Project B: Node 22 (new app)
# Both work simultaneously without nvm or n
# project-a/flake.nix
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
outputs = { self, nixpkgs }:
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
devShells.x86_64-linux.default = pkgs.mkShell {
packages = [ pkgs.nodejs_18 ]; # Node 18 only for this project
};
};
}
# project-b/flake.nix — same machine, different node version
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { self, nixpkgs }:
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
devShells.x86_64-linux.default = pkgs.mkShell {
packages = [ pkgs.nodejs_22 ]; # Node 22 only for this project
};
};
}
Pattern 2: Python Development with Poetry
# Fully reproducible Python environment with Poetry-managed deps
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
poetry2nix.url = "github:nix-community/poetry2nix";
};
outputs = { self, nixpkgs, poetry2nix }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
inherit (poetry2nix.lib.mkPoetry2Nix { inherit pkgs; }) mkPoetryApplication;
in
{
packages.${system}.default = mkPoetryApplication {
projectDir = ./.;
python = pkgs.python312;
};
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [ python312 poetry ruff mypy ];
shellHook = "poetry install";
};
};
}
Pattern 3: Home Manager for Dotfile Management
# ~/.config/home-manager/home.nix — manage user dotfiles and tools with Nix
{ config, pkgs, ... }:
{
home.username = "localuser";
home.homeDirectory = "/Users/localuser";
home.stateVersion = "24.05";
home.packages = with pkgs; [
gh ripgrep fd bat eza delta
jq yq-go httpie
python312 poetry
nodejs_22
];
programs.zsh = {
enable = true;
shellAliases = {
ls = "eza --icons";
cat = "bat";
grep = "rg";
};
initExtra = ''
eval "$(direnv hook zsh)"
'';
};
programs.git = {
enable = true;
userName = "Example User";
userEmail = "user@example.com";
delta.enable = true;
extraConfig.pull.rebase = true;
};
programs.direnv = {
enable = true;
nix-direnv.enable = true;
};
}
home-manager switch --flake ~/.config/home-manager#localuser
Pitfalls to Avoid
-
Mutating the Nix store or using nix-env -i in a flake project: nix-env -i installs packages imperatively to your user profile, bypassing the reproducibility benefits. In a flake project, always add packages to devShells.packages in flake.nix and run nix develop. The only exception: one-off tools you need for a few minutes (nix shell nixpkgs#htop).
-
Forgetting to commit flake.lock: The lockfile pins every input's exact git revision and hash. Without committing it, teammates get different versions even if they clone the same repo. Always commit flake.lock. Treat it like package-lock.json or Cargo.lock — it's not optional for reproducibility.
-
Building non-reproducible derivations: Nix sandboxes builds by default, but networking.enable = true; in a derivation or using builtins.currentTime defeats reproducibility. Keep derivations hermetic: all inputs must be declared, no network calls during build, no date or uname in build scripts. Use lib.fakeSha256 during first build to get the real hash, then replace it.
Related Skills
docker-expert — When Nix-built Docker images are deployed into orchestrated containers
senior-devops — Infrastructure-as-code patterns that Nix enables at the system level
rust-systems-programming — Rust projects with cargo2nix or crane for Nix-reproducible Cargo builds
ci-cd-pipeline-builder — Integrating Nix-based builds into GitHub Actions or GitLab CI
GitNexus Index
{
"skill": "nix-reproducible-builds",
"category": "devops",
"triggers": ["nix", "nixos", "nix flakes", "nix devshell", "nix home-manager", "reproducible builds", "nix docker", "nix develop", "flake.nix", "nix overlays"],
"outputs": ["flake.nix", "flake.lock", "devShell", "NixOS configuration.nix", "dockerTools image", "home.nix"],
"complexity": "high",
"tools": ["nix", "nixos", "home-manager", "direnv", "cachix", "nix-darwin", "poetry2nix"]
}