Comprehensive Nix and NixOS Anti-Pattern Reference Guide
Introduction
This document serves as a comprehensive reference for identifying and avoiding anti-patterns in Nix and NixOS configurations. It covers language-level mistakes, system configuration issues, package management problems, development environment pitfalls, performance bottlenecks, and Home Manager misconfigurations. Each anti-pattern includes descriptions, explanations of why it’s problematic, correct alternatives, and code examples.
1. Nix Language Anti-patterns
1.1 Syntax and Type Anti-patterns
Unquoted URLs (Deprecated)
Problem: Using bare URLs without quotes is deprecated and causes parsing ambiguities.
# ❌ BAD - Unquoted URL
fetchurl {
url = https://example.com/file.tar.gz;
sha256 = "...";
}
# ✅ GOOD - Always quote URLs
fetchurl {
url = "https://example.com/file.tar.gz";
sha256 = "...";
}
Why problematic: RFC 45 deprecated unquoted URLs due to parsing ambiguities and static analysis difficulties.
Path Division Confusion
Problem: Nix interprets 6/3 as a path, not division.
# ❌ BAD - Parsed as relative path
result = 6/3; # Creates path "./6/3"
# ✅ GOOD - Use spacing for arithmetic
result = 6 / 3; # Returns 2
# OR
result = builtins.div 6 3;
Type Coercion in String Interpolation
Problem: Not all types can be directly interpolated into strings.
# ❌ BAD - Type coercion failures
let
number = 42;
boolean = true;
in {
badNumber = "${number}"; # Error: cannot coerce integer
badBoolean = "${boolean}"; # Error: cannot coerce boolean
}
# ✅ GOOD - Explicit conversion
let
number = 42;
boolean = true;
in {
goodNumber = "${toString number}"; # "42"
goodBoolean = "${toString boolean}"; # "1" or ""
}
1.2 Scoping and Variable Anti-patterns
Excessive with Usage
Problem: with at top level makes code hard to analyze and debug.
# ❌ BAD - Unclear variable origins
with (import <nixpkgs> {});
with lib;
with stdenv;
mkDerivation {
name = "example";
buildInputs = [ curl jq ]; # Where do these come from?
}
# ✅ GOOD - Explicit imports
let
pkgs = import <nixpkgs> {};
inherit (pkgs) lib stdenv;
in
stdenv.mkDerivation {
name = "example";
buildInputs = with pkgs; [ curl jq ]; # Limited, clear scope
}
Why problematic: Static analysis tools can’t determine variable sources, makes refactoring difficult, and creates ambiguity for newcomers.
Manual Assignment Instead of inherit
Problem: Verbose, repetitive, and error-prone.
# ❌ BAD - Manual repetitive assignment
let
pkgs = import <nixpkgs> {};
in {
curl = pkgs.curl;
jq = pkgs.jq;
git = pkgs.git;
}
# ✅ GOOD - Use inherit
let
pkgs = import <nixpkgs> {};
in {
inherit (pkgs) curl jq git;
}
1.3 Dangerous Builtins Usage
Import From Derivation (IFD)
Problem: Forces sequential evaluation, blocking the evaluator.
# ❌ BAD - IFD blocks evaluation
let
generatedConfig = pkgs.runCommand "config" {} ''
echo "some_value = 42" > $out
'';
configValue = builtins.readFile generatedConfig; # Forces build!
in
pkgs.writeText "app-config" configValue
# ✅ GOOD - Keep evaluation and building separate
let
generatedConfig = pkgs.runCommand "config" {} ''
echo "some_value = 42" > $out
'';
in
pkgs.runCommand "app-config" { inherit generatedConfig; } ''
cp $generatedConfig $out
''
Performance impact: Can increase evaluation time from seconds to hours for complex projects.
Reading Secrets During Evaluation
Problem: Secrets get copied to the world-readable Nix store.
# ❌ BAD - Exposes password in store
services.myservice = {
password = builtins.readFile "/secrets/password"; # INSECURE!
}
# ✅ GOOD - Reference paths for runtime loading
services.myservice = {
passwordFile = "/secrets/password"; # Read at runtime
}
# ✅ BETTER - Use secret management
age.secrets.myservice-password.file = ../secrets/password.age;
services.myservice.passwordFile = config.age.secrets.myservice-password.path;
1.4 rec and let Anti-patterns
Dangerous rec Usage
Problem: Easy to create infinite recursion with shadowing.
# ❌ BAD - Infinite recursion
rec {
a = 1;
b = let a = a + 1; in a; # Infinite recursion!
}
# ✅ GOOD - Explicit self-reference
let
attrset = {
a = 1;
b = attrset.a + 1;
};
in attrset
Empty or Collapsible let Expressions
Problem: Adds unnecessary visual clutter.
# ❌ BAD - Empty let
let
in {
name = "example";
}
# ✅ GOOD - Remove unnecessary let
{
name = "example";
}
2. NixOS Configuration Anti-patterns
2.1 Package Management Anti-patterns
Using nix-env for System Packages
Problem: Breaks declarative configuration and reproducibility.
# ❌ BAD - Imperative installation
nix-env -i firefox vim git
# ✅ GOOD - Declarative in configuration.nix
environment.systemPackages = with pkgs; [
firefox
vim
git
];
Why problematic: Packages installed via nix-env aren’t tracked in configuration, persist across rebuilds unexpectedly, and make rollbacks incomplete.
Misusing environment.systemPackages
Problem: Installing user-specific packages system-wide.
# ❌ BAD - Everything system-wide
environment.systemPackages = with pkgs; [
firefox # Should be user-specific
vscode # Development tool
spotify # Personal application
];
# ✅ GOOD - Proper separation
environment.systemPackages = with pkgs; [
wget curl git vim # System essentials only
];
users.users.alice.packages = with pkgs; [
firefox vscode spotify # User-specific
];
2.2 Security Anti-patterns
Running Services as Root Unnecessarily
Problem: Violates principle of least privilege.
# ❌ BAD - Service runs as root by default
systemd.services.myservice = {
serviceConfig = {
ExecStart = "${pkgs.myapp}/bin/myapp";
# No User specified - runs as root!
};
};
# ✅ GOOD - Dedicated user with hardening
users.users.myservice = {
isSystemUser = true;
group = "myservice";
};
users.groups.myservice = {};
systemd.services.myservice = {
serviceConfig = {
ExecStart = "${pkgs.myapp}/bin/myapp";
User = "myservice";
Group = "myservice";
# Process isolation
DynamicUser = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
# Capabilities restrictions
NoNewPrivileges = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
# Memory protections
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
LockPersonality = true;
};
};
Poor Firewall Configuration
Problem: Disabling firewall or opening unnecessary ports.
# ❌ BAD - Security nightmare
networking.firewall.enable = false;
# OR
networking.firewall.allowedTCPPorts = [ 1-65535 ];
# ✅ GOOD - Minimal port opening
networking.firewall = {
enable = true;
allowedTCPPorts = [ 80 443 ]; # Only what's needed
# Interface-specific rules
interfaces."enp3s0" = {
allowedTCPPorts = [ 5432 ]; # PostgreSQL on internal only
};
};
2.3 Module Organization Anti-patterns
Monolithic Configuration File
Problem: Everything in one large configuration.nix.
# ❌ BAD - 500+ line configuration.nix
{ config, pkgs, ... }: {
boot.loader.grub.enable = true;
networking.hostName = "myhost";
services.nginx.enable = true;
# ... hundreds more lines
}
# ✅ GOOD - Modular structure
/etc/nixos/
├── configuration.nix # Main entry point
├── hardware-configuration.nix
├── modules/
│ ├── networking.nix
│ ├── security.nix
│ └── users.nix
└── services/
├── nginx.nix
└── postgresql.nix
3. Package Management Anti-patterns
3.1 Overlay Anti-patterns
Incorrect final vs prev Usage
Problem: Causes infinite recursion or broken dependencies.
# ❌ BAD - Infinite recursion
final: prev: {
hello = final.hello.overrideAttrs (oldAttrs: {
postPatch = "...";
});
}
# ✅ GOOD - Use prev for overriding
final: prev: {
hello = prev.hello.overrideAttrs (oldAttrs: {
postPatch = "...";
});
}
Using rec in Overlays
Problem: Breaks composability and prevents later overrides.
# ❌ BAD - rec breaks composability
final: prev: rec {
pkg-a = prev.callPackage ./a { };
pkg-b = prev.callPackage ./b { dependency-a = pkg-a; }
}
# ✅ GOOD - Reference through final
final: prev: {
pkg-a = prev.callPackage ./a { };
pkg-b = prev.callPackage ./b { dependency-a = final.pkg-a; };
}
3.2 Derivation Anti-patterns
Impure Derivations
Problem: Accessing network or filesystem during build.
# ❌ BAD - Network access during build
stdenv.mkDerivation {
name = "impure-build";
buildPhase = ''
curl -O https://example.com/dependency.tar.gz # WRONG!
'';
}
# ✅ GOOD - Pure build with fixed-output derivation
stdenv.mkDerivation {
name = "pure-build";
src = fetchurl {
url = "https://example.com/dependency.tar.gz";
sha256 = "..."; # Fixed output hash
};
}
Missing Phase Hooks
Problem: Breaking extensibility by not calling hooks.
# ❌ BAD - No hooks
installPhase = ''
mkdir -p $out/bin
cp myprogram $out/bin/
'';
# ✅ GOOD - Include hooks
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp myprogram $out/bin/
runHook postInstall
'';
Wrong Dependency Types
Problem: Confusing build-time and runtime dependencies.
# ❌ BAD - Wrong dependency placement
stdenv.mkDerivation {
buildInputs = [ gcc cmake ]; # Should be nativeBuildInputs!
}
# ✅ GOOD - Correct categorization
stdenv.mkDerivation {
nativeBuildInputs = [ gcc cmake ]; # Build tools
buildInputs = [ openssl zlib ]; # Runtime libraries
propagatedBuildInputs = [ essential-lib ]; # Propagated to consumers
}
4. Development Environment Anti-patterns
4.1 Flake Anti-patterns
Everything in flake.nix
Problem: Creates unmaintainable rightward drift.
# ❌ BAD - All code in flake.nix
{
outputs = { self, nixpkgs }: {
packages.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.stdenv.mkDerivation {
# 100+ lines of derivation code
};
};
}
# ✅ GOOD - Modular structure
{
outputs = { self, nixpkgs }: {
packages.x86_64-linux.default =
nixpkgs.legacyPackages.x86_64-linux.callPackage ./package.nix { };
};
}
Cross-compilation Incompatible Outputs
Problem: Hardcoding system prevents cross-compilation.
# ❌ BAD - System-specific
packages.${system}.myPackage = pkgs.stdenv.mkDerivation { ... };
# ✅ GOOD - Use overlays
overlays.default = final: prev: {
myPackage = final.callPackage ./package.nix { };
};
4.2 Shell Environment Anti-patterns
Mixing Build and Runtime Dependencies
Problem: Wrong categorization breaks cross-compilation.
# ❌ BAD - gcc in buildInputs
mkShell {
buildInputs = [ gcc nodejs ]; # gcc should be nativeBuildInputs
}
# ✅ GOOD - Proper categorization
mkShell {
nativeBuildInputs = [ gcc cmake pkg-config ]; # Build tools
buildInputs = [ openssl postgresql ]; # Runtime libraries
}
Blocking direnv Operations
Problem: Slow .envrc freezes shells and editors.
# ❌ BAD - Blocks for 5+ seconds
nix-shell --run 'direnv dump > .envrc.cache'
# ✅ GOOD - Use nix-direnv
use flake # With nix-direnv installed
Rule: .envrc should execute in <500ms.
4.3 CI/CD Anti-patterns
No Binary Cache Usage
Problem: Rebuilding everything from scratch.
# ❌ BAD - No caching
- name: Build
run: nix build
# ✅ GOOD - With caching
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build
run: nix build
Performance impact: Can reduce CI times by 20-40%.
5. Performance and Maintenance Anti-patterns
5.1 Evaluation Performance
Import From Derivation (IFD)
Problem: Blocks parallel evaluation.
# ❌ BAD - Sequential evaluation
let
generated = pkgs.runCommand "gen" {} "echo 'data' > $out";
in
import generated # Blocks here!
# ✅ GOOD - Pre-generate or use flakes
# Generate files outside of evaluation
Impact: Can increase evaluation from seconds to hours.
Excessive Thunk Creation
Problem: Memory usage and stack overflow risks.
# ❌ BAD - Thunk per iteration
let
values = map (x: let y = 1 + 1; in x) [1 2 3];
in values
# ✅ GOOD - Extract computations
let
y = 1 + 1;
values = map (x: x) [1 2 3];
in values
5.2 Store Management
Never Running Garbage Collection
Problem: Store grows unbounded (100GB+).
# ❌ BAD - No automated cleanup
# ✅ GOOD - Automated management
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 30d";
};
nix.optimise = {
automatic = true;
dates = [ "03:45" ];
};
Poor Binary Cache Configuration
Problem: Missing or misconfigured caches cause rebuilds.
# ❌ BAD - Wrong public key
nix.settings = {
substituters = [ "https://cache.example.org" ];
trusted-public-keys = [ "wrong-key" ]; # Breaks everything!
};
# ✅ GOOD - Proper cache setup
nix.settings = {
substituters = [
"https://cache.nixos.org/"
"https://nix-community.cachix.org"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
];
};
5.3 Update Strategies
Unsafe System Updates
Problem: Direct production updates without testing.
# ❌ BAD - Risky
nixos-rebuild switch --upgrade
# ✅ GOOD - Test first
nixos-rebuild build # Build only
nixos-rebuild test # Test without making permanent
nixos-rebuild build-vm # Test in VM
nixos-rebuild switch # Apply when confident
6. Home Manager Anti-patterns
6.1 Integration Issues
Duplicate Package Management
Problem: Same packages in system and Home Manager.
# ❌ BAD - Duplication
# /etc/nixos/configuration.nix
environment.systemPackages = with pkgs; [ neovim git ];
# ~/.config/home-manager/home.nix
home.packages = with pkgs; [ neovim git ]; # Conflict!
# ✅ GOOD - Clear separation
# System: only system-wide essentials
# Home Manager: user-specific packages
Missing stateVersion
Problem: Most common Home Manager error.
# ❌ BAD - No stateVersion
{
programs.git.enable = true;
# Error: The option 'home.stateVersion' is used but not defined
}
# ✅ GOOD - Set once, don't change
{
home.stateVersion = "24.05"; # Set to current version when starting
programs.git.enable = true;
}
6.2 Dotfile Management
Mixing Managed and Unmanaged Configs
Problem: Partial adoption creates conflicts.
# ❌ BAD - Some files managed, some not
# Creates "file in the way" errors
# ✅ GOOD - Gradual migration
programs.bash = {
enable = true;
# Start by importing existing config
bashrcExtra = builtins.readFile ./bashrc.backup;
# Gradually migrate to native options
};
Choosing how to ship a dotfile
There are four ways to get a config file into place with Home Manager, and the right choice depends on whether a native module exists and whether you want the file to stay editable as a real file.
1. Best — a native module option, when one exists
# Typed, mergeable, and a host can override one setting without
# restating the whole file.
programs.git.settings = {
init.defaultBranch = "main";
pull.rebase = true;
};
wayland.windowManager.hyprland.settings = {
"$mod" = "SUPER";
bind = [ "$mod, Q, exec, alacritty" ];
};
2. Also good — .source, pointing at a real file in the repository
# Pure: the file is copied into the Nix store, so the flake fully
# describes the result.
home.file.".vimrc".source = ./dotfiles/vimrc;
xdg.configFile."niri/config.kdl".source = ./dotfiles/niri.kdl;
The file stays a real file on disk, so your editor’s LSP, syntax highlighting and formatter all work on it, and upstream documentation maps to it 1:1. Use this whenever no native module exists, or when the upstream format is large or awkward to express as Nix.
3. Escape hatch — mkOutOfStoreSymlink
home.file.".vimrc".source =
config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/dotfiles/.vimrc";
This symlinks to a path outside the Nix store, so edits take effect without a rebuild — genuinely useful while iterating on a config. The trade-off is real and worth stating plainly: the flake no longer describes the result, the target must already exist at that path on every machine, and a fresh clone will not reproduce your setup. Reach for it deliberately, not by default.
4. Avoid for anything sizeable — a long inline .text block
# ❌ Fine for three lines. Miserable at a hundred.
home.file.".vimrc".text = ''
set number
set expandtab
'';
Inside a Nix string you lose LSP, syntax highlighting and formatters for the
embedded language; everything sits one indent level in; '' and ${ need
escaping; and when something breaks you debug the generated file and then walk
back to the Nix source. A handful of lines is fine. A hundred-line block is a
worse outcome than either option 1 or option 2.
Note: an earlier version of this guide presented a long inline
.textblock as the “good” fix formkOutOfStoreSymlink. That was wrong, and community feedback rightly pushed back. The real distinction is in the store versus out of it —.source = ./fileis pure and keeps a real file, so it fixes the purity problem without inheriting the ergonomics problem.
7. Detection and Prevention Tools
Language Linters
-
statix: Detects 20+ anti-patterns automatically
statix check # Find issues statix fix # Auto-fix many problems -
nixfmt/alejandra: Enforce consistent formatting
Package Tools
-
nixpkgs-hammering: Detects packaging anti-patterns
nix run github:jtojnar/nixpkgs-hammering -- packagename -
nixpkgs-review: Tests package changes
nixpkgs-review pr 12345
System Tools
- systemd-analyze security: Audit service security
- lynis: Comprehensive security auditing
- nixos-rebuild build-vm: Safe testing environment
Performance Tools
- NIX_SHOW_STATS=1: Shows evaluation statistics
- nix path-info -S: Check closure sizes
- nix-tree: Visualize dependencies
8. Quick Reference Checklist
Language
- URLs are quoted
- No excessive
withstatements - Using
inheritwhere appropriate - No IFD in critical paths
- Secrets not read during evaluation
- Minimal
recusage
System Configuration
- No
nix-envfor system packages - Proper package separation (system vs user)
- Services run with minimal privileges
- Firewall enabled with minimal ports
- Modular configuration structure
Packages & Overlays
- Correct
finalvsprevusage - No
recin overlays - Pure derivations only
- Proper dependency categorization
- Phase hooks included
Development
- Modular flake structure
- Cross-compilation compatible
- Fast direnv integration
- CI caching configured
- Language-specific tools included
Performance
- Garbage collection automated
- Binary caches configured
- Store optimization enabled
- Safe update procedures
- No unnecessary IFD
Home Manager
- stateVersion set correctly
- No duplicate packages
- Gradual config migration
- Services properly isolated
- Clear system/user separation
Conclusion
This reference guide covers the most critical anti-patterns encountered in real-world Nix and NixOS usage. The key to avoiding these issues is understanding:
- Evaluation vs Build Phase: Keep them separate, avoid IFD
- Declarative Philosophy: Everything in configuration files
- Proper Scoping: Use the right tool for the right scope
- Security by Default: Principle of least privilege
- Performance Awareness: Understand evaluation costs
- Gradual Adoption: Don’t try to do everything at once
Success with Nix/NixOS requires patience, understanding of the underlying model, and adherence to community best practices. Use the detection tools, follow the patterns shown here, and always test changes in safe environments before deploying to production.