Flakes: The Future of Nix
Nix Flakes represent the next evolution in Nix package management, providing a more structured, reproducible approach to managing dependencies. This guide explores how to use Flakes effectively in DevOps workflows.
What Are Flakes?
Flakes are a Nix feature that provides a standardized approach to:
Dependency Management: Lock exact versions of dependencies
Composable Configuration: Create modular, reusable configurations
Reproducible Builds: Guarantee identical environments every time
Self-Contained Projects: Define complete development environments
A Flake is defined by a flake.nix
file and a corresponding flake.lock
file that pins exact dependency versions.
Basic Flake Structure
A minimal flake.nix
file looks like this:
{
description = "My project flake";
inputs = {
# Core dependencies
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
# Additional dependencies
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
# Development environment
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_20
yarn
];
};
# Packages
packages = {
default = self.packages.${system}.myapp;
myapp = pkgs.stdenv.mkDerivation {
name = "myapp";
version = "1.0.0";
src = ./src;
buildPhase = ''
# Build commands
'';
installPhase = ''
mkdir -p $out/bin
cp myapp $out/bin/
'';
};
};
# Apps (runnable packages)
apps.default = {
type = "app";
program = "${self.packages.${system}.myapp}/bin/myapp";
};
}
);
}
Flake Inputs and Outputs
Inputs
The inputs
section defines external dependencies:
inputs = {
# From GitHub
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
# From a specific commit
nixpkgs-unstable.url = "github:NixOS/nixpkgs/5233fd2ba76a3accb05b4104bd6e6dda4061a396";
# From local path
utils = {
url = "path:./nix-utils";
inputs.nixpkgs.follows = "nixpkgs"; # Ensure consistent nixpkgs
};
# With flake = false for non-flake repos
legacy-project = {
url = "github:example/legacy-project";
flake = false;
};
}
Outputs
The outputs
section defines what the flake provides:
outputs = { self, nixpkgs, ... }:
let
# Systems to support
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
# Helper to generate per-system outputs
forAllSystems = nixpkgs.lib.genAttrs systems;
# Package set for each system
pkgsFor = system: import nixpkgs { inherit system; };
in {
# NixOS configurations
nixosConfigurations = {
myserver = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./nixos/configuration.nix ];
};
};
# Per-system outputs
packages = forAllSystems (system: {
default = self.packages.${system}.myapp;
myapp = (pkgsFor system).callPackage ./pkgs/myapp.nix {};
});
# Development shells
devShells = forAllSystems (system: {
default = (pkgsFor system).mkShell {
buildInputs = with (pkgsFor system); [ go gopls ];
};
});
# Formatter for nix files
formatter = forAllSystems (system: (pkgsFor system).nixpkgs-fmt);
};
Using Flakes in DevOps Workflows
Development Environment Flake
Create consistent development environments across your team:
{
description = "Development environment for our microservices";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
# Define project-specific variables
projectName = "acme-services";
# Define common dependencies
commonDeps = with pkgs; [
docker-compose
kubectl
kubernetes-helm
jq
yq
git
];
# Define language-specific dependencies
goDeps = with pkgs; [ go gopls golangci-lint ];
nodeDeps = with pkgs; [ nodejs_20 yarn nodePackages.typescript ];
pythonDeps = with pkgs; [
(python311.withPackages (ps: with ps; [
pytest
requests
pyyaml
black
mypy
]))
];
in {
# Define multiple development shells
devShells = {
default = pkgs.mkShell {
name = "${projectName}-default";
buildInputs = commonDeps;
shellHook = ''
echo "${projectName} development environment"
echo "Run 'nix develop .#backend' for Go development"
echo "Run 'nix develop .#frontend' for Node.js development"
echo "Run 'nix develop .#scripts' for Python scripts development"
'';
};
# Backend (Go) development shell
backend = pkgs.mkShell {
name = "${projectName}-backend";
buildInputs = commonDeps ++ goDeps;
shellHook = ''
echo "Backend (Go) development environment ready!"
export GOPATH=$PWD/.go
export PATH=$GOPATH/bin:$PATH
mkdir -p $GOPATH/bin
'';
};
# Frontend (Node.js) development shell
frontend = pkgs.mkShell {
name = "${projectName}-frontend";
buildInputs = commonDeps ++ nodeDeps;
shellHook = ''
echo "Frontend (Node.js) development environment ready!"
export PATH=$PWD/node_modules/.bin:$PATH
# Set up local node_modules bin directory
mkdir -p ./.npm-global/bin
export NPM_CONFIG_PREFIX=$PWD/.npm-global
export PATH=$NPM_CONFIG_PREFIX/bin:$PATH
# Disable update notifier to keep environment clean
export NO_UPDATE_NOTIFIER=1
'';
};
# Scripts (Python) development shell
scripts = pkgs.mkShell {
name = "${projectName}-scripts";
buildInputs = commonDeps ++ pythonDeps;
shellHook = ''
echo "Scripts (Python) development environment ready!"
# Create and activate a virtual environment if it doesn't exist
if [ ! -d .venv ]; then
echo "Creating Python virtual environment..."
python -m venv .venv
fi
source .venv/bin/activate
'';
};
};
}
);
}
CI/CD Pipeline Flake
Define CI/CD configurations in a Flake:
{
description = "CI/CD pipeline configurations";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
# Define shared CI dependencies
ciDeps = with pkgs; [
docker
git
jq
curl
gnumake
openssh
];
# Create a common CI environment
mkCiEnv = extraPkgs: pkgs.buildEnv {
name = "ci-environment";
paths = ciDeps ++ extraPkgs;
};
in {
# CI environments for different jobs
packages = {
default = self.packages.${system}.build-env;
# Environment for building code
build-env = mkCiEnv (with pkgs; [
nodejs_20
yarn
typescript
]);
# Environment for testing
test-env = mkCiEnv (with pkgs; [
nodejs_20
yarn
chromium
xvfb-run
]);
# Environment for deployment
deploy-env = mkCiEnv (with pkgs; [
kubectl
kubernetes-helm
awscli2
terraform
sops
]);
};
# CI/CD scripts as apps
apps = {
default = self.apps.${system}.ci-build;
ci-build = {
type = "app";
program = toString (pkgs.writeShellScript "ci-build" ''
set -euo pipefail
echo "Building application..."
cd $GITHUB_WORKSPACE
yarn install --frozen-lockfile
yarn build
echo "Running tests..."
yarn test
echo "Build successful!"
'');
};
ci-deploy = {
type = "app";
program = toString (pkgs.writeShellScript "ci-deploy" ''
set -euo pipefail
echo "Deploying to Kubernetes..."
# Configure kubectl
echo "$KUBE_CONFIG" | base64 -d > kubeconfig.yaml
export KUBECONFIG="$PWD/kubeconfig.yaml"
# Deploy with Helm
helm upgrade --install myapp ./helm/myapp \
--namespace $DEPLOY_NAMESPACE \
--set image.tag=$GITHUB_SHA \
--set environment=$DEPLOY_ENV \
--wait
echo "Deployment successful!"
'');
};
};
}
);
}
Microservices Flake
Manage multiple services in a single repository:
{
description = "Microservices architecture";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
flake-utils.url = "github:numtide/flake-utils";
# Additional tools
crane = {
url = "github:ipetkov/crane/master";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, flake-utils, crane, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
craneLib = crane.lib.${system};
# Common dependencies for all services
commonDeps = with pkgs; [
docker-compose
openssl
];
# Build a Docker image for a service
buildDockerImage = { name, dir, port ? 8080 }:
pkgs.dockerTools.buildImage {
inherit name;
tag = "latest";
copyToRoot = pkgs.buildEnv {
name = "image-root";
paths = [
self.packages.${system}.${name}
pkgs.cacert
pkgs.tzdata
];
pathsToLink = [ "/bin" "/etc" "/tmp" ];
};
config = {
Cmd = [ "/bin/${name}" ];
ExposedPorts = {
"${toString port}/tcp" = {};
};
Env = [
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
"TZ=UTC"
];
WorkingDir = "/app";
Volumes = {
"/data" = {};
};
};
};
# Helper for Go services
buildGoService = { name, dir, port ? 8080 }: {
# Build the Go binary
${name} = craneLib.buildPackage {
pname = name;
version = "1.0.0";
src = ./${dir};
buildInputs = with pkgs; [ go_1_20 ];
};
# Build the Docker image
"${name}-image" = buildDockerImage {
inherit name dir port;
};
};
# Helper for Node.js services
buildNodeService = { name, dir, port ? 3000 }: {
# Build the Node.js package
${name} = pkgs.buildNpmPackage {
pname = name;
version = "1.0.0";
src = ./${dir};
npmDepsHash = "sha256-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
installPhase = ''
mkdir -p $out/bin
cp -r dist $out/dist
cp package.json $out/
cat > $out/bin/${name} << EOF
#!/bin/sh
exec ${pkgs.nodejs_20}/bin/node $out/dist/index.js "\$@"
EOF
chmod +x $out/bin/${name}
'';
};
# Build the Docker image
"${name}-image" = buildDockerImage {
inherit name dir port;
};
};
# Generate Terraform provider configurations
terraformConfig = pkgs.writeTextFile {
name = "terraform-providers.tf";
text = ''
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.0"
}
}
required_version = ">= 1.0"
}
provider "aws" {
region = var.aws_region
}
provider "kubernetes" {
config_path = var.kubeconfig_path
}
'';
};
# Define services
services = {
auth = { name = "auth-service"; dir = "services/auth"; port = 8081; };
api = { name = "api-gateway"; dir = "services/api"; port = 8080; };
users = { name = "user-service"; dir = "services/users"; port = 8082; };
frontend = { name = "frontend"; dir = "services/frontend"; port = 3000; };
};
in {
# Packages for all services
packages = {
default = self.packages.${system}.all;
# Meta-package for building all services
all = pkgs.runCommandNoCC "all-services" {} ''
mkdir -p $out
echo "All services built successfully" > $out/success
'';
# Terraform configuration
terraform = terraformConfig;
# Build individual Go services
auth-service = (buildGoService services.auth).auth-service;
api-gateway = (buildGoService services.api).api-gateway;
user-service = (buildGoService services.users).user-service;
# Build individual Node.js services
frontend = (buildNodeService services.frontend).frontend;
# Docker images
auth-service-image = (buildGoService services.auth)."auth-service-image";
api-gateway-image = (buildGoService services.api)."api-gateway-image";
user-service-image = (buildGoService services.users)."user-service-image";
frontend-image = (buildNodeService services.frontend)."frontend-image";
};
# Development environments
devShells = {
default = pkgs.mkShell {
buildInputs = with pkgs; [
go_1_20
nodejs_20
yarn
docker-compose
kubectl
terraform
];
};
};
# Apps for development
apps = {
# Development launcher
dev = {
type = "app";
program = toString (pkgs.writeShellScript "launch-dev" ''
${pkgs.docker-compose}/bin/docker-compose -f docker-compose.dev.yaml up
'');
};
# Service runner
auth = {
type = "app";
program = "${self.packages.${system}.auth-service}/bin/auth-service";
};
api = {
type = "app";
program = "${self.packages.${system}.api-gateway}/bin/api-gateway";
};
users = {
type = "app";
program = "${self.packages.${system}.user-service}/bin/user-service";
};
};
}
);
}
NixOS System Configuration with Flakes
Define your entire NixOS system in a flake:
{
description = "My NixOS configurations";
inputs = {
# Core dependencies
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
# System management
home-manager = {
url = "github:nix-community/home-manager/release-23.11";
inputs.nixpkgs.follows = "nixpkgs";
};
# Hardware detection
hardware.url = "github:NixOS/nixos-hardware";
# Secrets management
agenix = {
url = "github:ryantm/agenix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, nixpkgs-unstable, home-manager, hardware, agenix, ... }:
let
lib = nixpkgs.lib;
# System types to support
systems = [ "x86_64-linux" "aarch64-linux" ];
forAllSystems = lib.genAttrs systems;
# Nixpkgs configuration
nixpkgsConfig = {
allowUnfree = true;
permittedInsecurePackages = [ "openssl-1.1.1u" ];
};
# Helper for creating system configurations
mkSystem = { system, hostname, username ? "admin", extraModules ? [] }:
lib.nixosSystem {
inherit system;
modules = [
# Base configuration
./nixos/configuration.nix
# Machine-specific configuration
./nixos/hosts/${hostname}
# Hardware configuration
./nixos/hardware/${hostname}.nix
# User configuration via home-manager
home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.${username} = import ./home/${username}.nix;
}
# Secrets management
agenix.nixosModules.default
# Hostname configuration
{
networking.hostName = hostname;
nixpkgs.config = nixpkgsConfig;
# Make flake inputs accessible in NixOS configuration
_module.args = {
flake-self = self;
inputs = {
nixpkgs-unstable = nixpkgs-unstable;
agenix = agenix;
hardware = hardware;
};
};
}
] ++ extraModules;
};
# Define overlay for accessing unstable packages
overlays = {
default = final: prev: {
unstable = import nixpkgs-unstable {
system = prev.system;
config = nixpkgsConfig;
};
};
};
in {
# NixOS system configurations
nixosConfigurations = {
# Desktop system
desktop = mkSystem {
system = "x86_64-linux";
hostname = "desktop";
username = "developer";
extraModules = [
# Use desktop hardware profile
hardware.nixosModules.dell-xps-15-9500
# Desktop-specific modules
./nixos/modules/desktop.nix
./nixos/modules/gaming.nix
];
};
# Server configuration
server = mkSystem {
system = "x86_64-linux";
hostname = "server";
extraModules = [
./nixos/modules/server.nix
./nixos/modules/monitoring.nix
./nixos/modules/services/web.nix
./nixos/modules/services/database.nix
];
};
# Raspberry Pi configuration
rpi4 = mkSystem {
system = "aarch64-linux";
hostname = "rpi4";
extraModules = [
hardware.nixosModules.raspberry-pi-4
./nixos/modules/iot-gateway.nix
];
};
};
# Overlays
overlays = overlays;
# Development shells available on all platforms
devShells = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
default = pkgs.mkShell {
buildInputs = with pkgs; [
nixpkgs-fmt
nil
nixos-rebuild
];
};
}
);
# Formatter for all platforms
formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.nixpkgs-fmt);
};
}
Advanced Flake Techniques
Multi-Environment Deployments
Define different environments (dev, staging, production) with shared configurations:
{
description = "Multi-environment deployment flake";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
};
outputs = { self, nixpkgs, ... }:
let
lib = nixpkgs.lib;
# Define supported systems
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
# Helper to create per-system attributes
forAllSystems = lib.genAttrs supportedSystems;
# Helper to create full system configurations
mkSystem = { system, hostname, environment, region ? "us-east-1" }:
let
# Base nixosSystem configuration
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
};
# Environment-specific settings
envSettings = {
dev = {
logLevel = "debug";
monitoring = true;
highAvailability = false;
instanceType = "t3.medium";
domain = "dev.example.com";
replicas = 1;
};
staging = {
logLevel = "info";
monitoring = true;
highAvailability = true;
instanceType = "t3.large";
domain = "staging.example.com";
replicas = 2;
};
prod = {
logLevel = "warn";
monitoring = true;
highAvailability = true;
instanceType = "m5.large";
domain = "example.com";
replicas = 3;
};
};
# Get environment settings with defaults
env = envSettings.${environment};
in lib.nixosSystem {
inherit system;
modules = [
# Base configuration that applies to all instances
./modules/base.nix
# Role-specific configuration
./modules/roles/${hostname}.nix
# Environment-specific configuration
./modules/environments/${environment}.nix
# System configuration
{
networking.hostName = "${hostname}-${environment}";
# Pass environment settings to the configuration
_module.args = {
inherit env;
inherit environment;
inherit region;
# Create a unique name for the instance
instanceName = "${hostname}-${environment}-${region}";
};
# Define environment variables for the system
environment.variables = {
ENV = environment;
LOG_LEVEL = env.logLevel;
DOMAIN = env.domain;
};
}
];
};
# Define all the hosts we need to create
allHosts = [
{ hostname = "api"; role = "application"; }
{ hostname = "worker"; role = "worker"; }
{ hostname = "db"; role = "database"; }
];
# Define all environments
allEnvironments = [ "dev" "staging" "prod" ];
# Define all regions (for multi-region deployments)
allRegions = {
dev = [ "us-east-1" ];
staging = [ "us-east-1" ];
prod = [ "us-east-1" "eu-west-1" ];
};
# Generate all possible combinations of hosts/environments/regions
mkSystemConfigurations = system:
lib.listToAttrs (
lib.flatten (
builtins.map (environment:
builtins.map (region:
builtins.map (host:
lib.nameValuePair
# Generate a unique name for this configuration
"${host.hostname}-${environment}-${region}"
# Create the system configuration
(mkSystem {
inherit system environment region;
hostname = host.hostname;
})
) allHosts
) (allRegions.${environment} or [ "us-east-1" ])
) allEnvironments
)
);
# Generate deployment scripts for each environment
mkDeployScript = environment: system:
let
pkgs = nixpkgs.legacyPackages.${system};
# Get list of systems for this environment
systems = builtins.filter (name: builtins.match ".*-${environment}-.*" name != null)
(builtins.attrNames self.nixosConfigurations);
in
pkgs.writeShellScriptBin "deploy-${environment}" ''
set -e
echo "Deploying ${environment} environment..."
# Deploy all systems for this environment
${lib.concatMapStringsSep "\n" (system: ''
echo "Deploying ${system}..."
nixos-rebuild switch --flake .#${system} --target-host ${system}.${environment}.example.com
'') systems}
echo "Deployment complete!"
'';
in {
# Define all NixOS system configurations
nixosConfigurations =
(mkSystemConfigurations "x86_64-linux") //
(mkSystemConfigurations "aarch64-linux");
# Provide deployment scripts for each environment and system
packages = forAllSystems (system: {
default = self.packages.${system}."deploy-dev";
# Create deployment scripts for each environment
"deploy-dev" = mkDeployScript "dev" system;
"deploy-staging" = mkDeployScript "staging" system;
"deploy-prod" = mkDeployScript "prod" system;
});
# Provide formatters and checks
formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.nixpkgs-fmt);
# Simple checks to validate configurations
checks = forAllSystems (system:
let pkgs = nixpkgs.legacyPackages.${system};
in {
# Check Nix formatting
format = pkgs.runCommand "check-format" {} ''
${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt --check ${./.}
touch $out
'';
}
);
};
}
Composing Flakes with Registry Overrides
Compose multiple flakes with registry overrides to create a unified system:
{
description = "Composite flake with registry overrides";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
# Additional flakes with own nixpkgs inputs
monitoring-flake = {
url = "github:my-org/monitoring-flake";
# Override its nixpkgs to use ours
inputs.nixpkgs.follows = "nixpkgs";
};
kubernetes-flake = {
url = "github:my-org/kubernetes-flake";
inputs.nixpkgs.follows = "nixpkgs";
};
database-flake = {
url = "github:my-org/database-flake";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, monitoring-flake, kubernetes-flake, database-flake, ... }:
let
# Systems to support
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
# Helper to create system configurations
mkSystem = { system, modules ? [] }:
nixpkgs.lib.nixosSystem {
inherit system modules;
};
in {
# Registry overrides
nixosConfigurations = {
# Composite system that combines all flakes
full-system = mkSystem {
system = "x86_64-linux";
modules = [
# Base configuration
./modules/configuration.nix
# Import modules from other flakes
monitoring-flake.nixosModules.prometheus
monitoring-flake.nixosModules.grafana
kubernetes-flake.nixosModules.kubernetes
database-flake.nixosModules.postgresql
# Configure with settings specific to our deployment
{
services.prometheus = {
enable = true;
scrapeConfigs = [
{
job_name = "node";
static_configs = [{
targets = [ "localhost:9100" ];
}];
}
];
};
services.kubernetes = {
roles = [ "master" "node" ];
addons.dashboard.enable = true;
};
services.postgresql = {
enable = true;
package = nixpkgs.legacyPackages.x86_64-linux.postgresql_14;
};
}
];
};
};
# Make modules from this flake available to others
nixosModules = {
default = ./modules/default.nix;
customService = ./modules/custom-service.nix;
};
# Expose packages from all flakes
packages = forAllSystems (system: {
default = self.packages.${system}.combined-tools;
# Combine tools from all flakes into one meta-package
combined-tools = nixpkgs.legacyPackages.${system}.buildEnv {
name = "combined-tools";
paths = [
# Local packages
(nixpkgs.legacyPackages.${system}.callPackage ./pkgs/local-tool {})
# Packages from other flakes
monitoring-flake.packages.${system}.prometheus-toolbox
kubernetes-flake.packages.${system}.k8s-tools
database-flake.packages.${system}.db-migration-tool
];
};
});
# Expose apps from all flakes
apps = forAllSystems (system: {
default = self.apps.${system}.manage;
# Our own apps
manage = {
type = "app";
program = toString (nixpkgs.legacyPackages.${system}.writeShellScript "manage" ''
echo "Management interface for combined system"
echo "1. Monitoring tools"
echo "2. Kubernetes tools"
echo "3. Database tools"
read -p "Select an option: " option
case $option in
1) ${monitoring-flake.apps.${system}.monitor.program} ;;
2) ${kubernetes-flake.apps.${system}.kubectl-wrapper.program} ;;
3) ${database-flake.apps.${system}.db-cli.program} ;;
*) echo "Invalid option" ;;
esac
'');
};
# Re-export apps from other flakes
monitor = monitoring-flake.apps.${system}.monitor;
kubectl = kubernetes-flake.apps.${system}.kubectl-wrapper;
db-cli = database-flake.apps.${system}.db-cli;
});
};
}
Managing Flake Dependencies
Dependency Locking and Updates
Flakes use a flake.lock
file to pin exact dependencies:
# Create or update the lock file based on flake.nix
nix flake update
# Update a specific input
nix flake update nixpkgs
# Update with specific commits
nix flake lock --override-input nixpkgs github:NixOS/nixpkgs/5233fd2ba76a3accb05b4104bd6e6dda4061a396
Dependency Visualization
Visualize your flake dependencies:
nix flake show
nix flake metadata --json | jq
Flakes in CI/CD Pipelines
Configure CI/CD pipelines to use flakes:
# .github/workflows/build.yml
name: Build and Test
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.11
extra_nix_config: |
experimental-features = nix-command flakes
- name: Set up Cachix
uses: cachix/cachix-action@v12
with:
name: my-project
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build application
run: nix build .#app
- name: Run tests
run: nix flake check
# Deployment step for main branch only
- name: Deploy
if: github.ref == 'refs/heads/main'
run: nix run .#deploy
Real-World DevOps Projects with Flakes
NixOS Server Fleet Management
Example of a flake for managing a fleet of servers:
{
description = "Server Fleet Management";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
# For secrets management
agenix = {
url = "github:ryantm/agenix";
inputs.nixpkgs.follows = "nixpkgs";
};
# Deploy-rs for multi-target deployments
deploy-rs = {
url = "github:serokell/deploy-rs";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, agenix, deploy-rs, ... }:
let
# Helper for creating server configurations
mkServer = { name, ip, system ? "x86_64-linux", roles ? [], region ? "us-east-1" }:
let
# Standard NixOS configuration
nixosConfig = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
# Base configuration for all servers
./modules/base.nix
# Role-specific configurations
(nixpkgs.lib.mkIf (builtins.elem "web" roles) ./modules/roles/web.nix)
(nixpkgs.lib.mkIf (builtins.elem "db" roles) ./modules/roles/database.nix)
(nixpkgs.lib.mkIf (builtins.elem "cache" roles) ./modules/roles/cache.nix)
(nixpkgs.lib.mkIf (builtins.elem "monitoring" roles) ./modules/roles/monitoring.nix)
# Region-specific configuration
./modules/regions/${region}.nix
# Load secrets management
agenix.nixosModules.default
# Server-specific configuration
{
networking = {
hostName = name;
inherit ip;
firewall.enable = true;
};
# Make some contextual variables available to all modules
_module.args = {
serverName = name;
serverRoles = roles;
serverRegion = region;
};
}
];
};
in {
# NixOS configuration
config = nixosConfig;
# Deploy-rs node configuration
deploy.node = {
hostname = ip;
profiles.system = {
user = "root";
path = deploy-rs.lib.${system}.activate.nixos nixosConfig;
};
};
};
# Define all servers in the fleet
servers = {
# Web servers
web-1 = mkServer {
name = "web-1";
ip = "10.0.1.11";
roles = [ "web" ];
};
web-2 = mkServer {
name = "web-2";
ip = "10.0.1.12";
roles = [ "web" ];
};
# Database servers
db-1 = mkServer {
name = "db-1";
ip = "10.0.2.11";
roles = [ "db" ];
};
db-2 = mkServer {
name = "db-2";
ip = "10.0.2.12";
roles = [ "db" ];
};
# Cache server
cache-1 = mkServer {
name = "cache-1";
ip = "10.0.3.11";
roles = [ "cache" ];
};
# Monitoring server
monitor = mkServer {
name = "monitor";
ip = "10.0.4.11";
roles = [ "monitoring" ];
};
# European region server
eu-web-1 = mkServer {
name = "eu-web-1";
ip = "10.1.1.11";
roles = [ "web" ];
region = "eu-west-1";
};
};
in {
# Export NixOS configurations
nixosConfigurations = nixpkgs.lib.mapAttrs
(name: server: server.config)
servers;
# Export deploy-rs node configurations
deploy.nodes = nixpkgs.lib.mapAttrs
(name: server: server.deploy.node)
servers;
# Add some checks to ensure the deployments are valid
checks = builtins.mapAttrs
(system: deploy-rs.lib.${system}.deployChecks self.deploy)
{ x86_64-linux = {}; aarch64-linux = {}; };
# Helper apps for administration
apps.x86_64-linux = {
# Deploy all servers
deploy-all = {
type = "app";
program = toString (nixpkgs.legacyPackages.x86_64-linux.writeShellScript "deploy-all" ''
${deploy-rs.defaultPackage.x86_64-linux}/bin/deploy .
'');
};
# Deploy only web servers
deploy-web = {
type = "app";
program = toString (nixpkgs.legacyPackages.x86_64-linux.writeShellScript "deploy-web" ''
${deploy-rs.defaultPackage.x86_64-linux}/bin/deploy .#web-1
${deploy-rs.defaultPackage.x86_64-linux}/bin/deploy .#web-2
${deploy-rs.defaultPackage.x86_64-linux}/bin/deploy .#eu-web-1
'');
};
# Deploy only database servers
deploy-db = {
type = "app";
program = toString (nixpkgs.legacyPackages.x86_64-linux.writeShellScript "deploy-db" ''
${deploy-rs.defaultPackage.x86_64-linux}/bin/deploy .#db-1
${deploy-rs.defaultPackage.x86_64-linux}/bin/deploy .#db-2
'');
};
};
};
}
Best Practices for Flakes in Production
Pin Dependencies: Always commit your
flake.lock
and update dependencies deliberately.Modularize Configurations: Use the module system to break down complex configurations.
Layered Architecture: Structure flakes with clear layering:
Base system configuration
Role-specific modules (web server, database, etc.)
Environment-specific modules (dev, staging, prod)
Host-specific overrides
Test Before Deployment: Use
nix flake check
and write tests for your configurations.Use CI/CD Pipelines: Integrate flake-based builds into your CI/CD pipelines.
Document Inputs and Outputs: Add good descriptions and documentation to your flakes.
Use Flake Registry: Consider registering frequently used flakes in the global registry.
Cache Aggressively: Use binary caches (like Cachix) to speed up builds and deployments.
Prefer Small, Focused Flakes: Create separate flakes for different concerns and compose them.
Version Your Flakes: Tag releases for important configurations to enable rollbacks.
Flakes Command Reference
# Build a flake output
nix build .#<output>
# Run a flake app
nix run .#<app>
# Develop with a flake shell
nix develop .#<devShell>
# Check a flake
nix flake check
# Show flake outputs
nix flake show
# Update flake lock file
nix flake update
# Lock a specific input
nix flake lock --override-input <input> <value>
Conclusion
Nix Flakes represent a significant improvement in Nix's dependency management and reproducibility story. As a DevOps engineer, Flakes provide the tools needed to:
Create truly reproducible development environments
Manage complex system configurations
Deploy consistent infrastructure across environments
Maintain a fleet of machines with confidence
While Flakes are still evolving, they have rapidly become the preferred approach for serious Nix users. By embracing Flakes, you can leverage Nix's full power with a more structured, composable approach.
Further Resources
Last updated