A nixcoders.org blog, You stand at the foot of a mountain of code. Your machine is a complex, delicate ecosystem of compilers, interpreters, libraries, and tools. It works—for now. But you remember the last time. The “it worked on my machine” debacle. The OS upgrade that broke your entire toolchain. The week you lost untangling dependency hell. The new team member who spent three days just setting up their environment.
This fragility is the tax we pay for modern software development. We accept it as a given, a force of nature. We build elaborate, often brittle, scaffolding of Dockerfiles, setup.sh scripts, package.json files, and CI configurations, hoping it will hold. We are gardeners constantly fighting entropy in our own digital plots.
What if there was a different way? A way to declare, with absolute precision, the entire state of your development environment, your build processes, and your deployed systems? A way to make it reproducible, shareable, and reliable across every machine, from your laptop to a massive CI cluster?
This isn’t a hypothetical. This is the promise of Nix.
And this is the philosophy of A nixcoders.org blog: a sanctuary for the pragmatic idealist, the engineer who is tired of the chaos and ready to build with certainty. This blog is not just about learning the Nix language or the nix command; it’s about a fundamental shift in how we wield our tools. It’s about forging your own path.
What is A nixcoders.org blog? Unbundling the Concepts
“Nix” is a term that refers to several interconnected things. It’s crucial to separate them to understand the whole.
-
The Nix Language: A simple, pure, functional, domain-specific language used for writing expressions. It’s not for writing general-purpose applications, but for describing packages, configurations, and environments. Its purity is its power: a function called with the same arguments will always produce the same result, a property that is the bedrock of reproducibility.
-
The Nix Package Manager: This is the tool that interprets the Nix language, builds packages, and manages what we call the Nix store, located typically at
/nix/store. The Nix store is a grand, immutable cache. Every package, library, or script built by Nix gets its own unique directory in this store, named with a cryptographic hash of all its inputs (sources, dependencies, build flags, etc.). This is the magic trick:-
If two builds have the exact same inputs, they get the same hash and the same path. The build is skipped; the existing result is used. This enables massive caching and sharing.
-
Different versions or variants get different paths. They can coexist perfectly, without conflict.
-
Because the store is immutable, you can never accidentally break a dependency. That
gccyou built six months ago is still there, perfectly intact.
-
-
The NixOS Linux Distribution: An entire operating system built and configured by the Nix package manager and the Nix language. Your entire system configuration—from the kernel version and bootloader settings to the users and running services—is defined in a single, declarative Nix expression (
configuration.nix). A system upgrade becomes a atomic switch to a new, immutable generation of the OS. You can always roll back, perfectly. -
Nixpkgs: The massive, community-maintained repository of over 80,000 package definitions and countless system configuration modules. It’s the bedrock of the entire ecosystem, a testament to the power of a shared, reproducible build system.
The A nixcoders.org blog Philosophy: Why We’re Here
The official Nix documentation is comprehensive, but it can be dense and academic. The community is brilliant, but the learning curve is steep. Many who glimpse the potential of Nix turn back, daunted by the initial complexity.
A nixcoders.org blog exists to bridge that gap.
Our philosophy is built on a few core tenets:
-
Pragmatism Over Purity: We value a working, understandable solution that gets the job done today over a theoretically perfect one that takes weeks to implement. We’ll show you how to start small, perhaps just managing your development shells, without needing to rebuild your entire world in Nix on day one.
-
The “Why” Behind the “What”: We won’t just tell you to run
nix-shell -p hello. We’ll explain the model that makes it work. Understanding the philosophy of immutable stores and pure builds is more important than memorizing a hundred commands. -
For the Craftsman, Not Just the Academic: Nix is a tool for building real things. Our examples will focus on real-world problems: setting up a Node.js + PostgreSQL project, creating a CI pipeline for a Rust web service, or reliably deploying a Python data science environment.
-
Embrace the Journey: Adopting Nix is a journey of empowerment. It can be frustrating, but the payoff is a level of control and confidence you never thought possible. We’re here as your guides on that journey.
Part 1: The Foundation – Your First Forge
Let’s stop talking abstractly and start building. We’ll begin by installing Nix on any Linux or macOS system. (Windows users can use WSL2, which provides a fantastic Linux environment for Nix).
Installation and the Immutable Store
The standard way to install Nix is the multi-user installation:
sh <(curl -L https://nixos.org/nix/install) --daemon
After installation, open a new terminal. You now have the nix command and, more importantly, a /nix/store directory. This directory is your forge’s foundation. It starts empty, but every package you build will be stored here, perfectly isolated.
Let’s try our first incantation:
nix-shell -p hello
This command does something remarkable. It:
-
Looks up the “hello” package in the
nixpkgsrepository. -
Checks if that specific build of
hello(with all its exact dependencies) exists in your local store. If not, it either builds it from source or, more likely, downloads a pre-built binary from the NixOS binary cache. -
Drops you into a new Bash shell where the
hellobinary is available in yourPATH.
Type hello and you’ll see a friendly greeting. Now, exit the shell (exit). Try running hello again. It’s gone! This is a key insight: the nix-shell environment was temporary. The hello package, however, is still in your /nix/store, waiting to be used again. You haven’t polluted your global environment.
Your First shell.nix: Capturing an Environment
While nix-shell -p is great for one-offs, the real power comes from declaring your environment in a file. Create a file named shell.nix:
# shell.nix { pkgs ? import <nixpkgs> {} }: pkgs.mkShell { buildInputs = with pkgs; [ python311 python311Packages.pip python311Packages.virtualenv nodejs_20 postgresql_16 git ]; shellHook = '' echo "Welcome to your project environment!" echo "Python: $(python --version)" echo "Node: $(node --version)" echo "PostgreSQL is available." export MY_PROJECT_DB="my_project_dev" ''; }
This is a Nix expression. Let’s break it down:
-
{ pkgs ? import <nixpkgs> {} }:This is a function argument. It says, “I take an argument calledpkgs. If you don’t provide it, I’ll fetch the latestnixpkgsrepository by default.” -
pkgs.mkShell: This is a function fromnixpkgsdesigned specifically for creating these development shells. -
buildInputs: This is a list of packages you want available in the shell. Here, we’re bringing in specific versions of Python, Node.js, PostgreSQL, and Git. -
shellHook: This is a bash script that runs when you enter the shell. It’s perfect for printing messages, setting environment variables, or starting services.
Now, run nix-shell in the same directory. It will find the shell.nix file, evaluate it, and bring all those dependencies into your environment. You now have a completely isolated, reproducible development environment for your project.
Commit this shell.nix file to your repository. Any developer on your team, on any machine that supports Nix, can now run nix-shell and get the exact same tooling. No more “it worked on my machine.” You’ve just forged your first reliable tool.
Part 2: Sharpening the Tools – The Nix Language in Practice
To become a true nixcoder, you must develop an intuition for the Nix language. It’s different from imperative languages like Python or JavaScript, but its simplicity is its strength.
Core Concepts: Values, Functions, and Laziness
Let’s look at some basic data types:
# This is a comment. Let's assign some values. my-string = "hello world"; my-integer = 42; my-floating = 3.14159; my-path = ./my-file.txt; # Paths are a first-class type! my-list = [ 1 2 3 "four" ]; # Lists are heterogeneous. my-attrs = { # Attribute sets are key-value maps, like JSON objects. name = "nixcoder"; level = 9001; languages = [ "nix" "rust" "python" ]; };
Functions are the heart of the language. Here’s how you define and use them:
# A simple function that adds two numbers. add = a: b: a + b; # Let's use it. We call a function by applying it to arguments. result = add 1 2; # result is 3 # A function that operates on an attribute set. greet = { name, greeting ? "Hello" }: # 'greeting' has a default value. "${greeting}, ${name}!"; message1 = greet { name = "Alice"; }; # "Hello, Alice!" message2 = greet { name = "Bob"; greeting = "Hola"; }; # "Hola, Bob!"
Nix is lazy. Expressions are not computed until their value is actually needed. This allows for powerful abstractions, like defining an infinite list without crashing your computer.
The Power of Derivation: Defining How to Build Something
The most important function in Nix is derivation. It’s a low-level function that tells Nix how to build a single package. You’ll rarely use it directly; instead, you’ll use helpers from nixpkgs like stdenv.mkDerivation.
Let’s deconstruct a simple package definition (this is similar to what you’d find in nixpkgs):
{ stdenv, fetchurl }: # This function depends on 'stdenv' and 'fetchurl' stdenv.mkDerivation { pname = "hello"; version = "2.12.1"; src = fetchurl { url = "mirror://gnu/hello/hello-2.12.1.tar.gz"; sha256 = "sha256-jZkUKv2SV28wsM18tCqNxoCZmLxdYH2Idh9RLibH2yA="; }; nativeBuildInputs = [ ]; # Build-time dependencies buildInputs = [ ]; # Run-time dependencies buildPhase = '' ./configure --prefix=$out make ''; installPhase = '' make install ''; }
-
pnameandversion: Identify the package. -
src: Usesfetchurlto get the source code. Thesha256is critical—it ensures the source is exactly what we expect. If it changes, the hash will mismatch, and the build will fail. This is a core reproducibility guarantee. -
buildPhaseandinstallPhase: Define the shell commands to build and install the software. The$outvariable is a special Nix variable that points to the path in the/nix/storewhere this package will live.
When Nix builds this, it runs these phases in a pure, isolated environment. It only has access to the dependencies explicitly declared in nativeBuildInputs and buildInputs. There is no internet access. The build cannot accidentally depend on a library you have installed system-wide. This purity is what makes the result reproducible.
Part 3: The Artisan’s Workshop – Real-World Workflows
With the basics under our belt, let’s see how nixcoders apply this to daily work.
Reproducible Development Environments: Beyond shell.nix
While shell.nix is powerful, the newer flakes system provides even more reproducibility and composability. Flakes fix a key problem: the import <nixpkgs> {} in our shell.nix depends on a global, mutable channel (the NIX_PATH). What if my channel is from last week and yours is from today? We might get different versions of nixpkgs!
Flakes lock dependencies, much like package-lock.json or Cargo.lock.
A basic flake has a flake.nix file:
# flake.nix { description = "A dev environment flake"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; in { devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ python311 nodejs_20 postgresql_16 ]; shellHook = ''...''; }; # We can also define packages and apps here! packages.default = pkgs.hello; # `nix build` will build hello. } ); }
Now, you can run nix develop to enter the shell. The exact versions of all inputs (like nixpkgs) are locked in a flake.lock file. This environment is now 100% reproducible on any machine, at any time.
Building and Packaging Your Own Projects
Imagine you have a simple Rust web service. You can define how to build it right inside your project’s flake.
# Inside flake.nix # ... (inputs and other structure) outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; in { packages.default = pkgs.rustPlatform.buildRustPackage { pname = "my-webservice"; version = "0.1.0"; src = ./.; # The source of the flake itself # This is the hash of your Cargo.lock-dependent source. # Nix can prefetch dependencies if it knows this. cargoLock.lockFile = ./Cargo.lock; # Optional: build-time dependencies nativeBuildInputs = with pkgs; [ pkg-config ]; # Optional: run-time dependencies (e.g., for linking) buildInputs = with pkgs; [ openssl ]; }; } );
Now, from this directory, you can run:
-
nix build: This will build your entire Rust project, fetching all its dependencies in a reproducible way. -
nix build .#my-webservice: This is an explicit way to build the package.
You’ve just created a single command that can build your project from a clean slate, on any platform. Your CI system can use this exact same command. The build is guaranteed to be identical.
Declarative System Configuration with NixOS
This is where the paradigm shift becomes truly profound. On NixOS, your entire machine is defined in a Nix expression, typically /etc/nixos/configuration.nix.
# /etc/nixos/configuration.nix { config, pkgs, ... }: { imports = [ # Include other configuration modules ./hardware-configuration.nix ]; boot.loader.systemd-boot.enable = true; networking.hostName = "my-forge"; # Declare the users on the system users.users.nixcoder = { isNormalUser = true; extraGroups = [ "wheel" "docker" ]; packages = with pkgs; [ firefox vscode # Your custom package from above! (callPackage /path/to/your/project/flake.nix {}) ]; }; # Declare the system-wide services services.openssh.enable = true; services.postgresql = { enable = true; package = pkgs.postgresql_16; }; # The version of the system itself system.stateVersion = "23.11"; # It's best to keep this stable. }
To apply this configuration, you run sudo nixos-rebuild switch. NixOS does the following:
-
It builds a complete, new system profile based on this expression, including the Linux kernel, all systemd services, and user environments. All of it is stored safely in the
/nix/store. -
It updates the bootloader menu to include this new generation.
-
It atomically switches the running system to use the new configuration. All declared services are restarted.
If you make a change that breaks your system (e.g., a bad config for a critical service), you can simply reboot and select the previous generation from the boot menu. You are back to a working system in minutes. You have effectively version control for your entire OS.
Part 4: The Guild – Advanced Patterns and Community
As you progress from apprentice to journeyman, you’ll discover patterns that solve complex problems.
Overlays and Overrides: Modifying Existing Packages
What if you need a version of a library that isn’t in nixpkgs, or you need to apply a patch? You don’t fork all of nixpkgs. You use an overlay.
# my-overlay.nix self: super: # 'self' is the final package set, 'super' is the previous one. { my-patched-hello = super.hello.overrideAttrs (oldAttrs: { patches = (oldAttrs.patches or []) ++ [ ./my-fix.patch ]; }); # A more complex override, building from a different source. my-nodejs = super.nodejs-20_x.override { version = "20.5.1-custom"; src = self.fetchurl { url = "https://internal.mirror/node-v20.5.1.tar.xz"; sha256 = "..."; }; }; }
You can then bring this overlay into your shell.nix, flake, or system configuration. This allows you to build a curated set of packages on top of the stable base of nixpkgs.
Home Manager: Declarative User Environments
You don’t have to use NixOS to benefit from declarative configs. Home Manager is a tool that uses Nix to manage your user environment—your dotfiles, shell configuration, and user-specific packages—on any Linux or macOS system.
# ~/.config/home-manager/home.nix { config, pkgs, ... }: { home.username = "nixcoder"; home.homeDirectory = "/home/nixcoder"; home.packages = with pkgs; [ neovim tmux htop jq ]; programs.git = { enable = true; userName = "nixcoder"; userEmail = "nixcoder@example.com"; }; programs.bash = { enable = true; shellAliases = { ll = "ls -l"; gs = "git status"; }; }; home.stateVersion = "23.11"; }
Run home-manager switch and your dotfiles are generated, your packages are installed, and your shell is configured. Your personal environment is now as reproducible as your projects.
The Path Forward
The journey of a nixcoder is one of continuous learning and empowerment. You start by taming a single development environment. You progress to building your own packages with absolute certainty. You might eventually take the leap to managing your entire OS with NixOS, achieving a level of system control and reliability that feels like a superpower.
It is a path that requires you to think differently, to value declaration over imperative steps, and to embrace the initial complexity for the profound simplicity that lies on the other side.
The models are not always perfect. The error messages can be cryptic. The community is still evolving best practices. But the direction is clear: towards a future where software is built and deployed not as a fragile, hand-crafted artifact, but as a precise, deterministic, and shareable derivation.
This is the forge we are building at A nixcoders.org blog. A place to learn, to share, and to hone our craft. So take up the hammer. Start with a simple shell.nix. Feel the power of the immutable store. And join us in forging a more reliable future, one derivation at a time.
