
"It works on my machine."
How many times have you heard this phrase? Or worse, how many times have you said it?
Picture this: You're two months into a project. Everything runs perfectly on your laptop. You push to production and disaster strikes. Wrong library version. Missing environment variable. Or your colleague just installed packages differently than you did.
The usual fix? Docker containers, virtual environments, documentation nobody reads, and hours of debugging. Been there way too many times.
But there's a system that solves this at its core: NixOS and Nix.
The question is: Is this another Linux trend, or is there something genuinely revolutionary here?
Let's find out.
Two things exist here: Nix (the package manager) and NixOS (the operating system). You can use one without the other.
Nix builds packages in complete isolation from each other. If a package works on one machine, it will work on another. No exceptions.
Instead of dumping packages into /usr/bin like every other package manager, Nix stores everything in /nix/store with a unique cryptographic hash:
/nix/store/nawl092prjblbhvv16kxxbk6j9gkgcqm-git-2.47.1
/nix/store/x8dhcf2kpl5h4lm9ci3k2y9vn8qlx9cd-nodejs-24.1.0
That random string? It's a hash of everything used to build the package source files, dependencies, compiler flags, all of it. Change anything, get a different hash, get a different package. I know, sounds nerdy but stick with me.
The Nix Formula
Think of it as a deterministic build process where specific inputs always generate the exact same, immutable output:
SOURCE CODE
DEPENDENCIES
BUILD SCRIPT
NIX HASH FUNCTION
UNIQUE STORE PATH
/nix/store/x8dhkfewprojgihtrmvcf2...-nodejs-24.1.0
git-source + openssl-3.0 → Result:..5yn92-gitgit-source + openssl-3.1 → Result:..fx23f-gitEven a tiny security patch in a dependency (like moving from OpenSSL 3.0 to 3.1) creates a completely new, isolated path. Your old version stays untouched, and the new one doesn't overwrite it.
NixOS extends this concept to your entire system. Everything kernel, services, packages, configs lives in declarative files.
Traditional approach (imperative):
sudo apt install nginx
sudo systemctl enable nginx
# manually edit configs
# hope you documented this
# pray nothing breaks
NixOS approach (declarative):
{
services.nginx.enable = true;
services.nginx.virtualHosts."example.com" = {
root = "/var/www";
};
}
Run sudo nixos-rebuild switch and your system becomes that description. That's it.
The /nix/store is read-only and immutable. Nothing gets deleted or modified Nix creates new versions and symlinks to them.
This is why rollbacks are instant. Every system state you've ever had is still there. Something breaks? Reboot, select the previous generation, back to working in 30 seconds. I've saved my own setup more times than I'd like to admit this way.
You're probably thinking: "Won't my disk fill up?"
Traditional Distros
When you run apt remove, packages are gone forever. Want it back? Reinstall. Made a mistake? Too bad. Orphaned dependencies? Hunt them down manually with apt autoremove.
Nix's Intelligence
Nothing gets deleted until you explicitly run garbage collection. And even then, it only removes what's truly unreachable nothing any generation or project depends on.
# Preview what would be deleted
nix-collect-garbage --dry-run
# Delete unreachable packages
nix-collect-garbage
# Delete old generations
nix-collect-garbage -d
# Delete generations older than 30 days
nix-collect-garbage --delete-older-than 30d
Automate it in NixOS:
{
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 30d";
};
}
Your last month of generations stays safe while everything older gets cleaned automatically.
Managing development environments is painful. Let's talk about the actual problems you face daily.
You're juggling three Node.js projects. One needs Node 20, another needs Node 22, the new microservice needs Node 24. You've got nvm but you keep forgetting to switch versions and waste time debugging. (Ask me how I know.)
nvm use → build failsWith Nix:
Just cd into a project directory. That's it. Nix sees the config and loads the exact environment needed. Automatically.
# Project A - Node 20
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_20
python311 # for native modules
];
}
Switch projects? Different Node version loads automatically. No thinking required.
Your colleague pushes a TypeScript project. You clone it, run npm install, compilation fails. Different TypeScript version. Missing build tool. Now you're debugging setup instead of coding.
The old way: Write documentation nobody reads. Debug the same issues with every new team member.
The Nix way: Share a flake.nix file. Run one command. Get the exact same setup.
{
description = "TypeScript project";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
outputs = { nixpkgs, ... }: {
devShells.x86_64-linux.default =
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_24
nodePackages.typescript
nodePackages.eslint
];
};
};
}
package.json + package-lock.json but for your entire development setup, not just JavaScript packages.Your Go project works locally. Push to CI? Fails. Different environment variables. Slightly different Go version. Manual configuration from months ago nobody remembers.
You end up with three different behaviors: your machine, your coworker's machine, and CI.
Nix fixes this: The environment is code. Same file, same environment everywhere.
{
description = "Go project";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
outputs = { nixpkgs, ... }: {
devShells.x86_64-linux.default =
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in pkgs.mkShell {
buildInputs = with pkgs; [
go_1_24
golangci-lint
delve
];
shellHook = ''
export GOPATH=$PWD/.go
export PATH=$GOPATH/bin:$PATH
'';
};
};
}
This file goes in your repo. Everyone who uses it local devs, CI, staging, production gets the exact same setup. The inconsistency just disappears.
Nix treats every package as a pure function a mathematical function where the same inputs always produce the exact same output.
inputs (source + dependencies + tools) → cryptographic hash → output package
Change anything in the inputs, get a different hash, get a different package.
/nix/store: Isolation at ScaleEvery package lives in /nix/store with a unique hash:
$ ls /nix/store/ | grep nodejs
3j8d...-nodejs-24.1.0
7k2m...-nodejs-22.11.0
9xp4...-nodejs-20.18.1
Those prefixes are content-addressable hashes representing everything used to build that package. This is why multiple versions coexist without conflicts.
Traditional systems are imperative you tell them what to do step by step:
# Steps that modify state
sudo apt update
sudo apt install nginx
sudo systemctl enable nginx
sudo vim /etc/nginx/nginx.conf
# Hope you documented this
# Pray it works on another machine
Nix is declarative you describe what you want:
{
services.nginx = {
enable = true;
virtualHosts."myapp.com" = {
root = "/var/www/myapp";
};
};
}
Run nixos-rebuild switch and your system transforms to match that description.
When you update your system:
Boot menu shows your history:
NixOS - Generation 47 (current)
NixOS - Generation 46
NixOS - Generation 45
...
Something broke? Boot into the previous generation or run:
sudo nixos-rebuild switch --rollback
30 seconds. Done. No reinstalling packages, no configuration restoration, no database migrations. Just instant time travel.
They solve different problems and work great together.
Docker's Strength
Runtime Isolation
Packages your app with everything it needs. Ship the container, runs identically everywhere. Massive ecosystem.
Weaknesses:
RUN apt-get install fetches latest versions (not deterministic)Nix's Strength
Build Reproducibility
Every package built from exact pinned dependencies. Cryptographic guarantees.
Weaknesses:
{ pkgs ? import <nixpkgs> {} }:
pkgs.dockerTools.buildLayeredImage {
name = "my-app";
tag = "latest";
contents = with pkgs; [ nodejs_24 bash ];
config.Cmd = [ "${pkgs.nodejs_24}/bin/node" "app.js" ];
}
| Feature | apt/yum/brew | Nix |
|---|---|---|
| Approach | Imperative (do this, then that) | Declarative (system should be this) |
| Reproducibility | None state drifts over time | Perfect same config = same system |
| Multiple Versions | Conflicts | Coexist peacefully |
| Rollbacks | None | Instant (30 seconds) |
| Version Control | Manual documentation | Git-based configs |
Traditional package managers modify global state. Nix builds deterministic state.
Ubuntu/Debian
Best for: General use, "just works" experience
Strengths: Massive community, everything documented, stable LTS releases
Weaknesses: State drifts, updates break things, no atomic rollbacks
Arch Linux
Best for: Cutting-edge packages, DIY approach
Strengths: Latest software, extensive wiki, complete control
Weaknesses: Manual everything, breaks occasionally, no safety net
Fedora
Best for: Modern tech, balanced approach
Strengths: Latest features, Red Hat backing, SELinux integration
Weaknesses: 6-month release cycle, traditional package management
NixOS
Best for: Reproducibility, declarative infrastructure
Strengths: Atomic rollbacks, perfect reproducibility, version control everything
Weaknesses: Steep learning curve, smaller community, weird syntax (seriously, functional programming in a config file?)
Every language has version managers: nvm, pyenv, rbenv, gvm.
Problems:
Nix replaces all of them:
{
description = "Multi-language project";
outputs = { nixpkgs, ... }: {
devShells.x86_64-linux.default =
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_24 # JavaScript
python311 # Python
go_1_24 # Go
rustc # Rust
postgresql_16 # Database
redis # Cache
];
};
};
}
One tool. All languages. System dependencies included. Completely reproducible.
Other systems claim reproducibility. Nix enforces it mathematically.
flake.lock = identical environment everywhere. Not "similar." Not "should work." Identical. Cryptographically guaranteed.Every change creates a generation. Every generation is recoverable.
# List all generations
nix-env --list-generations
# Rollback to previous
sudo nixos-rebuild switch --rollback
# Switch to specific generation
sudo nixos-rebuild switch --rollback 42
Broke something? You're 30 seconds away from a working state. Always.
Not "infrastructure as code" aspirationally. Actually as code.
# This IS your system. Not a description. The actual state.
{
services.nginx.enable = true;
services.postgresql.enable = true;
services.redis.enable = true;
networking.firewall.allowedTCPPorts = [ 80 443 ];
users.users.deploy = {
isNormalUser = true;
extraGroups = [ "wheel" ];
};
}
Check this into Git. Every machine built from this file is identical.
Share a flake.nix. Everyone gets the exact environment. Not documentation. Not instructions. The actual environment.
git clone → nix develop → coding in 2 minutes. No setup docs. No "it works on my machine." Just instant productivity.Build once. Share everywhere. Teams using Cachix build packages once and everyone else downloads pre-built binaries.
First developer builds? 20 minutes. Everyone else? 30 seconds.
Problem: New developer spends two days installing tools, debugging versions, configuring environment.
Solution: One file, one command.
{
description = "Backend API dev environment";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
outputs = { nixpkgs, ... }: {
devShells.x86_64-linux.default =
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_24 python311 go_1_24
postgresql_16 redis
docker-compose kubectl
];
shellHook = ''
export DATABASE_URL="postgresql://localhost/myapp"
echo "Dev environment loaded!"
'';
};
};
}
Real companies: Replit, Shopify, Tweag use this for instant reproducible dev environments.
Problem: 50 servers. One configured differently. Behaves differently. Nobody knows why.
Solution: Entire server config in declarative file checked into Git.
nixos-rebuild switch --target-host api-prod-01
All 50 servers identical. Changes version-controlled. Rollback any change instantly.
Problem: Three machines with different configs. Syncing manually is tedious.
Solution: Declare your entire user environment.
{ pkgs, ... }: {
home.packages = with pkgs; [
neovim tmux git ripgrep fzf bat
];
programs.git = {
enable = true;
userName = "Your Name";
userEmail = "you@example.com";
};
programs.zsh = {
enable = true;
shellAliases = {
ll = "ls -la";
gs = "git status";
};
};
}
New laptop? Run a few commands. Five minutes later, entire setup restored perfectly.
Flakes are exactly like package.json + package-lock.json combined but for your entire development environment.
Traditional Nix: import <nixpkgs> imports whatever version is on your system. Your coworker might have different version. CI might have yet another.
Flakes fix this: flake.lock pins exact versions.
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1704067270,
"narHash": "sha256-...",
"rev": "63678e9f3d3afecfeafa0acead6239cdb54055e4"
}
}
}
}
Result: Same flake.lock = identical environment everywhere. No surprises. Ever.
Manage your entire user environment declaratively. Not just packages everything. Editor configs, shell aliases, Git settings, SSH keys.
Changed your config? home-manager switch applies it instantly. Don't like it? home-manager switch --rollback reverts it.
It's literally version control for your entire workspace.
Let's be real about the problems.
No single source of truth. Information scattered. Examples don't always work with current versions. You'll spend hours debugging something better docs could've solved in minutes.
Ubuntu has millions of users. NixOS? Significantly smaller community.
In practice, that looks like:
Don't use NixOS if:
First build might take forever. Updating your system could take 30 minutes if lots changed. Binary caches help, but that first experience is brutal. Pro tip: don't start a system rebuild right before a meeting.
Start with just the Nix package manager on your existing system. Don't jump into full NixOS immediately.
Recommended installer (enables Flakes by default):
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
Verify:
nix --version
# Try cowsay temporarily
nix shell nixpkgs#cowsay
cowsay "Hello Nix!"
# Install permanently
nix profile install nixpkgs#firefox
{
description = "My first Nix environment";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
outputs = { nixpkgs, ... }: {
devShells.x86_64-linux.default =
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in pkgs.mkShell {
buildInputs = with pkgs; [ nodejs_24 python311 ];
};
};
}
Use it:
git init
git add flake.nix
nix develop
Perfect development environment. That's it.
After you're comfortable with Nix for a few weeks, try NixOS in a VM first. Don't rush this step. NixOS is a commitment.
Honestly? It depends.
If you've ever:
Then yes, Nix is revolutionary. The reproducibility actually works. The atomic rollbacks actually work. The declarative configuration actually works.
But if you just need a stable desktop that works and don't care about reproducibility, then the hype doesn't apply to you. And that's okay. Ubuntu and Fedora serve most people perfectly.
Nix isn't about being trendy. It's about reliability through mathematics. Your system becomes a pure function: same inputs → same outputs, guaranteed.
For teams building production systems or requiring strict reproducibility, Nix provides value that's hard to quantify. The initial investment pays dividends daily.
Try it. Break it. Learn from it. Then decide for yourself. That's what I did anyway.
The best part? With Nix, you can always roll back.