Nix provides powerful tools for building, packaging, and distributing software in a reproducible manner. This guide details practical approaches to building packages with Nix in a DevOps context.
Anatomy of a Nix Package
A Nix package is defined by a derivation, which specifies all inputs needed to build the package:
{ stdenv, fetchurl, perl }:
stdenv.mkDerivation {
name = "hello-2.12";
# Source code
src = fetchurl {
url = "https://ftp.gnu.org/gnu/hello/hello-2.12.tar.gz"\;
hash = "sha256-zoJ2IvKo2ioO2U13kTX780rP2MaGwIGcbFOvG+Oi17Y=";
};
# Build-time dependencies
buildInputs = [ perl ];
# Configuration flags
configureFlags = [ "--with-debug" ];
# Custom build steps (if default is insufficient)
buildPhase = ''
make -j $NIX_BUILD_CORES
'';
# Installation instructions
installPhase = ''
make install
mkdir -p $out/share/doc
cp README* $out/share/doc/
'';
# Meta information for package discovery and maintenance
meta = {
description = "A program that produces a friendly greeting";
homepage = "https://www.gnu.org/software/hello/"\;
license = stdenv.lib.licenses.gpl3Plus;
maintainers = [ "example@example.com" ];
platforms = stdenv.lib.platforms.all;
};
}
Common Build Phases in Nix
Phase
Default Action
Custom Example
unpackPhase
Extract source archive
tar -xf $src; cd myproject-*
patchPhase
Apply patches
patch -p1 < $patchfile
configurePhase
Run ./configure
./configure --prefix=$out --enable-feature
buildPhase
Run make
make -j $NIX_BUILD_CORES CFLAGS="-O3"
checkPhase
Run tests
make test
installPhase
Run make install
make install DESTDIR=$out
fixupPhase
Fix runtime paths
Automatic
Fetching Source Code
Nix provides several fetchers to obtain source code:
# From a URL with hash verification
src = fetchurl {
url = "https://example.org/package-1.0.tar.gz"\;
hash = "sha256-1234...";
};
# From Git with specific revision
src = fetchGit {
url = "https://github.com/user/repo.git"\;
rev = "abcdef123456789";
sha256 = "sha256-5678...";
};
# From GitHub with simplified syntax
src = fetchFromGitHub {
owner = "user";
repo = "project";
rev = "v1.0.0";
sha256 = "sha256-9abc...";
};
# From local path (for development)
src = ./path/to/source;
Test packages in isolation using nix-build or nix build:
# Build the package
nix-build -A mypackage
# Run a specific test attribute
nix-build -A mypackage.tests.integration
# Build with a specific version of nixpkgs
nix-build -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/23.05.tar.gz -A mypackage
CI Configuration for Package Building
Example GitHub Actions workflow for building Nix packages:
name: Build Nix Packages
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Nix
uses: cachix/install-nix-action@v20
with:
nix_path: nixpkgs=channel:nixos-23.05
- name: Set up Cachix
uses: cachix/cachix-action@v12
with:
name: my-company-cache
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build packages
run: |
nix-build -A mypackage1 --option substitute true
nix-build -A mypackage2 --option substitute true
- name: Run tests
run: nix-build -A mypackage.tests
Advanced Topics
Cross-Compilation
Build packages for different architectures:
{ pkgs ? import <nixpkgs> {} }:
let
# Create a custom pkgs instance for the target architecture
pkgsCross = import pkgs.path {
crossSystem = {
config = "aarch64-unknown-linux-gnu";
};
};
in
# Build your package using the cross-compilation toolchain
pkgsCross.callPackage ./default.nix {}
Static Linking
Create statically linked binaries for containerized environments:
{ stdenv, lib, fetchFromGitHub, buildGoModule }:
buildGoModule rec {
pname = "static-app";
version = "1.0.0";
src = fetchFromGitHub { /* ... */ };
vendorSha256 = "sha256-abc123...";
# Configure Go to build a fully static binary
ldflags = [
"-s" "-w"
"-extldflags '-static'"
];
# Ensure CGO is disabled for true static linking
CGO_ENABLED = 0;
# Additional flags for truly static binaries
tags = [ "netgo" "osusergo" ];
meta = with lib; {
description = "Statically linked application for containers";
platforms = platforms.linux;
};
}
Creating Minimal Docker Images
Use Nix to create minimal Docker images:
{ pkgs ? import <nixpkgs> {} }:
let
myapp = pkgs.callPackage ./default.nix {};
in
pkgs.dockerTools.buildImage {
name = "myapp";
tag = "latest";
# Only include the necessary components
contents = [
myapp
pkgs.cacert # SSL certificates
pkgs.tzdata # Timezone data
];
# Configure entry point
config = {
Cmd = [ "${myapp}/bin/myapp" ];
WorkingDir = "/data";
Volumes = {
"/data" = {};
};
ExposedPorts = {
"8080/tcp" = {};
};
};
}
Best Practices for Package Maintainability
Properly Set Metadata: Always include complete meta attributes:
meta = with lib; {
description = "Concise yet comprehensive description";
homepage = "https://project-website.com/"\;
license = licenses.mit; # Or appropriate license
maintainers = with maintainers; [ yourname ];
platforms = platforms.linux;
};
Version Management: Use rec pattern for version propagation:
Minimize Closure Size: For deployments, especially in containers, minimize runtime dependencies:
# Only include runtime dependencies, not build-time ones
propagatedBuildInputs = [ necessary-lib ];
# Strip debug symbols
postInstall = ''
$STRIP $out/bin/myapp
'';
Conclusion
Building packages with Nix provides a powerful, reproducible approach to software packaging for DevOps environments. By following these patterns and best practices, you can create reliable builds that work consistently across development, testing, and production environments.