NixOS and Nixpkgs Patterns Guide¶
Comprehensive best practices guide for writing idiomatic Nix code, based on official documentation from nix.dev and the Nixpkgs Manual.
Table of Contents¶
- Module System Patterns
- Package Writing Patterns
- Configuration Patterns
- Security Patterns
- Performance Patterns
Module System Patterns¶
1. Module Structure and Evaluation¶
Pattern: Function-Based Modules
Modules should be functions that receive arguments and return attribute sets with options and config sections.
{ config, lib, pkgs, ... }:
let
cfg = config.services.myservice;
in
{
options.services.myservice = {
enable = lib.mkEnableOption "MyService";
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Port for MyService to listen on";
};
};
config = lib.mkIf cfg.enable {
systemd.services.myservice = {
description = "My Service";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.myservice}/bin/myservice --port ${toString cfg.port}";
DynamicUser = true;
};
};
};
}
Why this works:
- Clear separation between options (what users configure) and config (what gets activated)
- The module system handles merging automatically
- Lazy evaluation allows circular dependencies to resolve correctly
2. Type System Best Practices¶
Pattern: Choose Appropriate Types
Use the right type for your data to enable automatic validation and merging.
options = {
# Simple types
enable = lib.mkEnableOption "feature"; # Shorthand for bool with default false
name = lib.mkOption { type = lib.types.str; };
count = lib.mkOption { type = lib.types.int; };
# Constrained types
port = lib.mkOption {
type = lib.types.port; # Integer 0-65535
};
logLevel = lib.mkOption {
type = lib.types.enum [ "debug" "info" "warn" "error" ];
default = "info";
};
age = lib.mkOption {
type = lib.types.ints.between 0 120;
};
# Collection types
users = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
};
settings = lib.mkOption {
type = lib.types.attrsOf lib.types.anything;
default = {};
};
# Pattern validation
email = lib.mkOption {
type = lib.types.strMatching ".+@.+\\..+";
};
# Optional types
apiKey = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
};
};
Merging Behavior by Type:
str: Single definition only (error on multiple)lines: Multiple definitions concatenated with newlineslistOf: Lists merged by concatenationattrsOf: Attribute sets merged recursivelybool: Error on conflicting definitions
3. Submodules for Nested Configuration¶
Pattern: Use Submodules for Complex Structures
When options have multiple related fields, use submodules to group them logically.
options.services.myapp = {
instances = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({ name, config, ... }: {
options = {
enable = lib.mkEnableOption "this instance";
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
};
port = lib.mkOption {
type = lib.types.port;
# Use the instance name as part of default
default = 8000 + (lib.toInt name);
};
workers = lib.mkOption {
type = lib.types.ints.positive;
default = 4;
};
};
}));
default = {};
};
};
# Usage:
config.services.myapp.instances = {
web = {
enable = true;
port = 8080;
workers = 8;
};
api = {
enable = true;
port = 8081;
workers = 4;
};
};
Special submodule arguments:
name: The attribute name when used withattrsOfconfig: The submodule's own config- Access parent config via the parent's
configargument
4. Module Organization¶
Pattern: Split Options from Implementation
Separate option declarations from their implementations using the imports attribute.
# modules/myservice/default.nix
{
imports = [
./options.nix # Option declarations
./implementation.nix # Config implementation
];
}
# modules/myservice/options.nix
{ lib, ... }:
{
options.services.myservice = {
# All options declared here
};
}
# modules/myservice/implementation.nix
{ config, lib, pkgs, ... }:
let
cfg = config.services.myservice;
in
{
config = lib.mkIf cfg.enable {
# Implementation here
};
}
Benefits:
- Clear separation of interface (options) and implementation (config)
- Easier to review changes to public API
- Options can be shared across multiple implementations
5. Priority and Defaults¶
Pattern: Use mkDefault for Overridable Defaults
Use lib.mkDefault for values that should be easy to override without conflicts.
config = lib.mkIf cfg.enable {
# High priority - hard to override (avoid unless necessary)
networking.hostName = lib.mkForce "myhost";
# Normal priority - conflicts if set elsewhere
services.nginx.enable = true;
# Low priority - easily overridden (PREFERRED for defaults)
services.nginx.user = lib.mkDefault "nginx";
services.nginx.group = lib.mkDefault "nginx";
# Very low priority - absolute fallback
networking.firewall.enable = lib.mkOverride 1500 true;
};
Priority levels:
mkForce= 50 (very high priority, avoid)- Normal = 100 (default)
mkDefault= 1000 (low priority, good for defaults)mkOverride <n>= custom priority
6. Conditional Configuration¶
Pattern: Use mkIf for Conditional Blocks
Use lib.mkIf to conditionally include entire configuration sections.
config = lib.mkMerge [
# Always applied when service is enabled
(lib.mkIf cfg.enable {
systemd.services.myservice = {
description = "My Service";
wantedBy = [ "multi-user.target" ];
};
})
# Only when SSL is enabled
(lib.mkIf (cfg.enable && cfg.ssl.enable) {
systemd.services.myservice.environment = {
SSL_CERT = cfg.ssl.certFile;
SSL_KEY = cfg.ssl.keyFile;
};
})
# Development mode settings
(lib.mkIf (cfg.enable && cfg.mode == "development") {
systemd.services.myservice.environment.DEBUG = "1";
networking.firewall.allowedTCPPorts = [ cfg.port ];
})
];
Key principle: Never use mkIf condition true - use direct boolean assignment instead.
7. Cross-Module Configuration¶
Pattern: Access Other Module Options via config
Modules can read and respond to options defined in other modules.
{ config, lib, ... }:
let
cfg = config.services.myservice;
in
{
config = lib.mkIf cfg.enable {
# React to nginx being enabled
services.nginx.virtualHosts = lib.mkIf config.services.nginx.enable {
"myservice.local" = {
locations."/".proxyPass = "http://localhost:${toString cfg.port}";
};
};
# Adapt to firewall settings
systemd.services.myservice.after = lib.mkIf config.networking.firewall.enable [
"firewall.service"
];
};
}
8. Making Values Available Across Modules¶
Pattern: Use _module.args for Shared Dependencies
Make custom values available to all modules using _module.args.
{ lib, pkgs, ... }:
{
_module.args = {
# Make custom package set available
myPkgs = import ./pkgs { inherit pkgs; };
# Share utility functions
mkService = name: { /* ... */ };
# Provide computed values
hostInfo = {
isProduction = true;
domain = "example.com";
};
};
imports = [
# All imported modules can now access myPkgs, mkService, hostInfo
./services
./networking
];
}
9. Type-Safe Configuration Generation¶
Pattern: Use Format Generators
For configuration files, use pkgs.formats for type-safe generation.
{ config, lib, pkgs, ... }:
let
cfg = config.services.myservice;
# Choose appropriate format
configFormat = pkgs.formats.yaml {}; # or .json, .toml, .ini
configFile = configFormat.generate "myservice.yaml" {
inherit (cfg) host port;
database = {
inherit (cfg.database) host port name;
# Never include passwords in store
passwordFile = cfg.database.passwordFile;
};
};
in
{
options.services.myservice = {
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
};
port = lib.mkOption {
type = lib.types.port;
default = 8080;
};
database = lib.mkOption {
type = lib.types.submodule {
options = {
host = lib.mkOption { type = lib.types.str; };
port = lib.mkOption { type = lib.types.port; };
name = lib.mkOption { type = lib.types.str; };
passwordFile = lib.mkOption { type = lib.types.path; };
};
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.myservice = {
serviceConfig = {
ExecStart = "${pkgs.myservice}/bin/myservice --config ${configFile}";
# Load password at runtime
LoadCredential = "db-password:${cfg.database.passwordFile}";
};
};
};
}
10. Assertions and Warnings¶
Pattern: Validate Configuration with Assertions
Use assertions to catch configuration errors early with helpful messages.
{ config, lib, ... }:
let
cfg = config.services.myservice;
in
{
config = lib.mkIf cfg.enable {
# Hard requirements - build will fail
assertions = [
{
assertion = cfg.database.host != "";
message = "services.myservice.database.host must be set";
}
{
assertion = cfg.port != cfg.adminPort;
message = "services.myservice.port and adminPort must be different";
}
{
assertion = cfg.ssl.enable -> (cfg.ssl.certFile != null && cfg.ssl.keyFile != null);
message = "services.myservice.ssl requires certFile and keyFile";
}
];
# Soft warnings - build continues but warns user
warnings = lib.optional (cfg.ssl.enable == false)
"services.myservice: SSL is disabled - not recommended for production";
warnings = lib.optional (cfg.workers < 2)
"services.myservice: Running with less than 2 workers may impact reliability";
};
}
Package Writing Patterns¶
1. Standard Derivation Structure¶
Pattern: Use stdenv.mkDerivation Properly
Follow the standard structure for building packages.
{ lib
, stdenv
, fetchFromGitHub
, cmake
, pkg-config
, openssl
, zlib
}:
stdenv.mkDerivation rec {
pname = "myapp";
version = "1.0.0";
src = fetchFromGitHub {
owner = "myorg";
repo = pname;
rev = "v${version}";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
# Build-time dependencies
nativeBuildInputs = [
cmake
pkg-config
];
# Runtime dependencies
buildInputs = [
openssl
zlib
];
# Strict dependency separation
strictDeps = true;
# CMake flags
cmakeFlags = [
"-DENABLE_TESTS=OFF"
"-DCMAKE_BUILD_TYPE=Release"
];
# Environment variables
env.NIX_CFLAGS_COMPILE = "-O3";
# Use phase hooks
postPatch = ''
# Patch files before building
substituteInPlace Makefile \
--replace /usr/local $out
'';
postInstall = ''
# Additional installation steps
wrapProgram $out/bin/myapp \
--prefix PATH : ${lib.makeBinPath [ openssl ]}
'';
meta = with lib; {
description = "Short description of the package";
longDescription = ''
Longer description that can span
multiple lines.
'';
homepage = "https://myapp.example.com";
changelog = "https://github.com/myorg/myapp/releases/tag/v${version}";
license = licenses.mit;
maintainers = with maintainers; [ myname ];
platforms = platforms.linux;
mainProgram = "myapp";
};
}
Key principles:
- Use
recsparingly or not at all nativeBuildInputsfor build toolsbuildInputsfor runtime libraries- Always include comprehensive
metaattributes - Use
runHook preInstallandrunHook postInstallin custom phases
2. Dependency Management¶
Pattern: CallPackage for Automatic Injection
Use callPackage to automatically inject dependencies.
# pkgs/myapp/default.nix
{ lib, buildGoModule, fetchFromGitHub, installShellFiles }:
buildGoModule rec {
pname = "myapp";
version = "1.0.0";
# Dependencies automatically injected by callPackage
# ...
}
# In all-packages.nix or overlay
{
myapp = callPackage ./pkgs/myapp { };
# Override specific dependencies
myappCustom = callPackage ./pkgs/myapp {
buildGoModule = buildGo122Module; # Use specific Go version
};
}
3. Override Patterns¶
Pattern: Provide Both override and overrideAttrs
Make packages customizable with proper override support.
# Good - packages built with mkDerivation automatically support both
# Override function arguments
myPackageCustom = myPackage.override {
enableFeatureX = true;
openssl = openssl_3;
};
# Override derivation attributes (PREFERRED)
myPackagePatched = myPackage.overrideAttrs (oldAttrs: {
patches = (oldAttrs.patches or []) ++ [
./my-custom.patch
];
postInstall = (oldAttrs.postInstall or "") + ''
# Additional installation steps
'';
});
Why overrideAttrs is preferred:
- Preserves all derivation processing by
stdenv.mkDerivation - Allows override of build phases, dependencies, and attributes
- Maintains proper attribute merging
4. Multi-Output Packages¶
Pattern: Split Large Packages into Multiple Outputs
Reduce closure size by splitting development files, documentation, etc.
stdenv.mkDerivation rec {
pname = "mylib";
version = "1.0.0";
# Define outputs
outputs = [ "out" "dev" "doc" "man" ];
# out: runtime files (libraries, binaries)
# dev: headers, pkg-config files
# doc: documentation
# man: man pages
postInstall = ''
# Move development files
moveToOutput "include" "$dev"
moveToOutput "lib/pkgconfig" "$dev"
# Move documentation
moveToOutput "share/doc" "$doc"
moveToOutput "share/man" "$man"
'';
meta = {
# Specify which output contains the main program
outputsToInstall = [ "out" ];
};
}
# Usage:
environment.systemPackages = [
mylib # Just runtime files
mylib.dev # Development files
mylib.doc # Documentation
];
5. Language-Specific Builders¶
Pattern: Use Specialized Builders for Each Language
Nixpkgs provides optimized builders for different languages.
# Python
{ buildPythonApplication, fetchPypi, requests, pytest }:
buildPythonApplication rec {
pname = "myapp";
version = "1.0.0";
src = fetchPypi {
inherit pname version;
hash = "sha256-...";
};
propagatedBuildInputs = [ requests ];
nativeCheckInputs = [ pytest ];
pythonImportsCheck = [ "myapp" ];
}
# Node.js
{ buildNpmPackage, fetchFromGitHub }:
buildNpmPackage rec {
pname = "myapp";
version = "1.0.0";
src = fetchFromGitHub { /* ... */ };
npmDepsHash = "sha256-...";
dontNpmBuild = false; # Run npm build
}
# Go
{ buildGoModule, fetchFromGitHub }:
buildGoModule rec {
pname = "myapp";
version = "1.0.0";
src = fetchFromGitHub { /* ... */ };
vendorHash = "sha256-...";
ldflags = [
"-s" "-w"
"-X main.version=${version}"
];
}
# Rust
{ rustPlatform, fetchFromGitHub }:
rustPlatform.buildRustPackage rec {
pname = "myapp";
version = "1.0.0";
src = fetchFromGitHub { /* ... */ };
cargoHash = "sha256-...";
nativeBuildInputs = [ /* ... */ ];
buildInputs = [ /* ... */ ];
}
6. Overlay Patterns¶
Pattern: Create Composable Overlays
Use overlays to extend or modify the package set systematically.
# Simple overlay
final: prev: {
# Add new package
myCustomPackage = final.callPackage ./pkgs/my-custom-package { };
# Override existing package
hello = prev.hello.overrideAttrs (oldAttrs: {
patches = (oldAttrs.patches or []) ++ [ ./hello.patch ];
});
# Compose with other packages
myApp = final.callPackage ./pkgs/my-app {
specialLib = final.myCustomPackage;
};
}
# Overlay with configuration
{ enableFeature ? false }:
final: prev: {
myApp = prev.myApp.override {
inherit enableFeature;
};
}
# System-wide overlays in NixOS
nixpkgs.overlays = [
(import ./overlays/custom-packages.nix)
(import ./overlays/patched-packages.nix)
];
Overlay argument conventions:
final(orself): The final package set after all overlaysprev(orsuper): The package set from the previous overlay- Always use
finalfor dependencies - Use
prevonly when modifying an existing package
7. Testing Patterns¶
Pattern: Include Tests in Package Definitions
Use nixpkgs testers for validation.
{ lib
, stdenv
, fetchFromGitHub
, testers
, myapp # Self-reference for passthru.tests
}:
stdenv.mkDerivation rec {
pname = "myapp";
version = "1.0.0";
src = fetchFromGitHub { /* ... */ };
doCheck = true;
checkPhase = ''
runHook preCheck
./test-suite
runHook postCheck
'';
# Passthru tests run separately
passthru.tests = {
version = testers.testVersion {
package = myapp;
command = "myapp --version";
};
# Custom test
basic-functionality = testers.runNixOSTest {
name = "myapp-basic";
nodes.machine = { pkgs, ... }: {
environment.systemPackages = [ pkgs.myapp ];
};
testScript = ''
machine.succeed("myapp --help")
machine.succeed("myapp check")
'';
};
};
# Update script for automated version bumps
passthru.updateScript = nix-update-script { };
meta = with lib; {
# ...
};
}
8. Shell Script Wrapping¶
Pattern: Use writeShellApplication for Scripts
For shell scripts that need dependencies, use writeShellApplication.
{ lib, pkgs, writeShellApplication, curl, jq, coreutils }:
writeShellApplication {
name = "my-script";
runtimeInputs = [
curl
jq
coreutils
];
text = ''
# Script has curl, jq, and coreutils in PATH
response=$(curl -s "https://api.example.com/data")
echo "$response" | jq '.items[] | .name' | sort
'';
# Automatic shellcheck validation
checkPhase = ''
${lib.getExe pkgs.shellcheck} $target
'';
}
9. Cross-Compilation Support¶
Pattern: Proper Build vs Host Dependencies
Support cross-compilation by correctly categorizing dependencies.
{ lib
, stdenv
, buildPackages # For cross-compilation
, cmake
, pkg-config
, zlib
}:
stdenv.mkDerivation {
pname = "myapp";
version = "1.0.0";
strictDeps = true; # Essential for cross-compilation
# depsBuildBuild: build platform → build platform
depsBuildBuild = [
buildPackages.setuptools # Tools used during build on build platform
];
# nativeBuildInputs: build platform → host platform
nativeBuildInputs = [
cmake # Runs on build platform
pkg-config # Runs on build platform
];
# buildInputs: host platform → host platform
buildInputs = [
zlib # Library linked into final binary
];
# For cross-compilation, use buildPackages when needed
configurePhase = ''
${buildPackages.cmake}/bin/cmake .
'';
}
Configuration Patterns¶
1. Reproducible System Configuration¶
Pattern: Declare Everything in configuration.nix
{ config, pkgs, ... }:
{
# System state version - set once, never change
system.stateVersion = "24.05";
# All system packages declared
environment.systemPackages = with pkgs; [
vim git curl wget
];
# All services configured
services.openssh.enable = true;
# All users declared
users.users.alice = {
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" ];
# Password set via passwordFile, not inline
hashedPasswordFile = "/etc/nixos/secrets/alice-password";
};
# Network configuration
networking = {
hostName = "myhost";
networkmanager.enable = true;
};
}
2. Modular Configuration with Imports¶
Pattern: Split Configuration into Logical Modules
/etc/nixos/
├── configuration.nix
├── hardware-configuration.nix
├── modules/
│ ├── boot.nix
│ ├── networking.nix
│ ├── security.nix
│ └── users.nix
└── services/
├── web.nix
├── database.nix
└── monitoring.nix
# configuration.nix
{ config, pkgs, ... }:
{
imports = [
./hardware-configuration.nix
./modules/boot.nix
./modules/networking.nix
./modules/security.nix
./modules/users.nix
./services/web.nix
./services/database.nix
./services/monitoring.nix
];
system.stateVersion = "24.05";
}
3. Home Manager Integration¶
Pattern: Separate User and System Configuration
# System configuration
environment.systemPackages = with pkgs; [
# Only system-wide essentials
vim git wget curl
];
# Home Manager configuration
home-manager.users.alice = { pkgs, ... }: {
home.stateVersion = "24.05";
# User-specific packages
home.packages = with pkgs; [
firefox
vscode
spotify
];
# User program configuration
programs.git = {
enable = true;
userName = "Alice";
userEmail = "alice@example.com";
};
programs.zsh = {
enable = true;
oh-my-zsh = {
enable = true;
theme = "robbyrussell";
};
};
};
4. Feature Flags for Optional Functionality¶
Pattern: Use Feature Flags for Host Capabilities
{ config, lib, pkgs, ... }:
let
features = {
isLaptop = true;
hasNvidiaGpu = false;
isServer = false;
enableGaming = true;
};
in
{
# Power management for laptops
services.tlp.enable = features.isLaptop;
services.thermald.enable = features.isLaptop;
# GPU drivers
services.xserver.videoDrivers = lib.mkIf features.hasNvidiaGpu [ "nvidia" ];
# Server-specific services
services.fail2ban.enable = features.isServer;
# Gaming packages
environment.systemPackages = lib.mkIf features.enableGaming (with pkgs; [
steam
discord
lutris
]);
}
Security Patterns¶
1. Systemd Service Hardening¶
Pattern: Apply Comprehensive Service Isolation
systemd.services.myservice = {
description = "My Service";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
# User isolation
DynamicUser = true; # Automatic user/group creation
User = "myservice";
Group = "myservice";
# Filesystem isolation
ProtectSystem = "strict"; # Read-only /usr, /boot, /etc
ProtectHome = true; # Inaccessible /home
PrivateTmp = true; # Private /tmp
ReadWritePaths = [ "/var/lib/myservice" ]; # Only necessary paths
# Process restrictions
NoNewPrivileges = true; # Can't gain new privileges
PrivateDevices = true; # No access to /dev
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RestrictSUIDSGID = true;
# Capability restrictions
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
# System call filtering
SystemCallFilter = [ "@system-service" "~@privileged" ];
SystemCallArchitectures = "native";
# Resource limits
MemoryMax = "1G";
TasksMax = 1000;
# Network restrictions (if service doesn't need network)
PrivateNetwork = false; # Set true if no network needed
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
ExecStart = "${pkgs.myservice}/bin/myservice";
};
};
2. Secret Management¶
Pattern: Never Include Secrets in Store
# NEVER DO THIS
services.myservice = {
password = "hardcoded-password"; # INSECURE!
};
# Use file references
services.myservice = {
passwordFile = "/run/secrets/myservice-password";
};
# Or use agenix/sops-nix
age.secrets.myservice-password = {
file = ./secrets/myservice-password.age;
mode = "0400";
owner = "myservice";
group = "myservice";
};
services.myservice = {
passwordFile = config.age.secrets.myservice-password.path;
};
# systemd credentials
systemd.services.myservice = {
serviceConfig = {
LoadCredential = "password:/etc/myservice/password";
# Access via $CREDENTIALS_DIRECTORY/password
};
};
3. Firewall Configuration¶
Pattern: Minimal Port Opening
networking.firewall = {
enable = true;
# Global rules - minimal
allowedTCPPorts = [ 22 ]; # SSH only
# Interface-specific rules
interfaces = {
# Internal network
"enp3s0" = {
allowedTCPPorts = [ 5432 9090 3000 ]; # Database, Prometheus, Grafana
};
# External network
"enp4s0" = {
allowedTCPPorts = [ 80 443 ]; # HTTP/HTTPS only
};
};
# Custom rules using iptables
extraCommands = ''
# Rate limit SSH connections
iptables -A nixos-fw -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --set
iptables -A nixos-fw -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 4 -j DROP
'';
};
Performance Patterns¶
1. Build Optimization¶
Pattern: Configure Nix for Optimal Performance
nix.settings = {
# Parallel builds
max-jobs = "auto"; # Use all available CPU cores
cores = 0; # Each job can use all cores
# Build optimization
keep-outputs = true; # Keep build outputs for debugging
keep-derivations = true; # Keep .drv files
# Sandbox for reproducibility
sandbox = true;
# Binary caches
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="
];
# Experimental features
experimental-features = [ "nix-command" "flakes" ];
};
2. Store Management¶
Pattern: Automated Garbage Collection
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 30d";
};
nix.optimise = {
automatic = true;
dates = [ "03:45" ]; # Run during low-usage time
};
# Keep specific generations
boot.loader.grub.configurationLimit = 10;
3. Evaluation Performance¶
Pattern: Lazy Evaluation and Minimal IFD
# GOOD - Lazy evaluation, no IFD
let
# Only evaluated if used
expensiveComputation = /* ... */;
in
{
# Computed only when myFeature is enabled
services.myservice.config = lib.mkIf config.features.myFeature {
value = expensiveComputation;
};
}
# BAD - Import From Derivation
let
generatedConfig = pkgs.runCommand "config" {} ''
echo "value=42" > $out
'';
configValue = builtins.readFile generatedConfig; # Forces build during eval!
in { }
Documentation Standards¶
1. Module Documentation¶
Pattern: Comprehensive Option Documentation
options.services.myservice = {
enable = lib.mkEnableOption "MyService daemon";
package = lib.mkPackageOption pkgs "myservice" {
default = pkgs.myservice;
example = lib.literalExpression "pkgs.myservice.override { enableFeature = true; }";
};
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
example = "0.0.0.0";
description = ''
Host address for MyService to bind to.
Use `localhost` for local connections only,
or `0.0.0.0` to accept connections from any address.
'';
};
settings = lib.mkOption {
type = with lib.types; attrsOf anything;
default = {};
example = lib.literalExpression ''
{
server = {
host = "0.0.0.0";
port = 8080;
};
database = {
url = "postgresql://localhost/myservice";
};
}
'';
description = ''
Configuration for MyService.
See <https://myservice.example.com/docs> for available options.
Settings are serialized to YAML format.
'';
};
};
2. Package Metadata¶
Pattern: Complete and Accurate Meta Attributes
meta = with lib; {
description = "Short one-line description";
longDescription = ''
Detailed description that can span multiple lines.
Explain what the package does, its key features,
and any important usage information.
'';
homepage = "https://example.com";
changelog = "https://github.com/org/repo/releases/tag/v${version}";
downloadPage = "https://example.com/downloads";
license = licenses.mit; # or licenses.gpl3, etc.
maintainers = with maintainers; [ your-github-username ];
platforms = platforms.linux; # or platforms.unix, platforms.all
# For packages with executables
mainProgram = "executable-name";
# Broken or unmaintained
broken = false;
# Security-sensitive packages
priority = 10; # Lower = higher priority in PATH
};
Summary¶
These patterns represent best practices from official Nix documentation and the NixOS community. Following them ensures:
- Type Safety: Proper use of the module system's type checking
- Composability: Modules and packages that work well together
- Maintainability: Clear, explicit code that's easy to understand
- Performance: Efficient evaluation and builds
- Security: Properly isolated services and secret management
- Documentation: Self-documenting code with comprehensive options
Always refer to the official documentation for the most up-to-date information: