1 of 136

Nix Shells: Supercharging Your Projects with Reproducibility, Reliability & Declarative Control

2 of 136

Attendees type & vibe check:

  1. NixOS users?
  2. Nix users?
  3. Linux users?
  4. DevOps people?
  5. Scripting people?
  6. Just curious people😃?

3 of 136

Before we start - The Prerequisites:

  • Any of the below systems with Nix Installed:
  • Linux(any distro of your choice),
  • Mac,
  • Windows (WSL- Windows subsystem Linux)

Nix installer link: (Determinate Nix Installer)

https://determinate.systems/posts/determinate-nix-installer/

OR

  • NixOS inside a VM (with flakes enabled)

4 of 136

Ready for Nix?

5 of 136

Let's Start🔥

6 of 136

Arc 1: The Imperative Curse & The Quest for Determinism

7 of 136

Decades of fundamental problems in the IT world

I used to keep a README 📄 full of setup steps.

Then I had a Makefile 🛠️. Then a Dockerfile 🐳.

Then a setup.sh, a requirements.txt 📦, and a ci.yml 🤖.

And still… things broke. 💣

Someone would clone the repo and ask:

"What version of Python does this need? 🐍"

"Do I install that with Brew 🍺 or Pip 📦?"

"Why doesn't the Docker build work on my machine? 💻"

8 of 136

Decades of fundamental problems in the IT world

In today's fragmented tooling landscape 🌐, projects rely on multiple moving parts:

Language-specific package managers

System setup scripts

CI configurations, etc

9 of 136

The culprit: IMPERATIVE (Creates Divergent system)

Imagine giving the same ingredients and recipe to many and somehow the food tastes different.

But, this difference is a pain in the IT world. We've become accustomed to this behavior through repeated troubleshooting and temporary fixes. And the more the tool stacks increase, hell seeps through these small cracks.

10 of 136

Even identical systems diverge over time, if started and maintained the same way.�

Adjustments and midway script fail rollbacks become frequent habits.

11 of 136

Dependency Hell:

Traditional PMs- Global dependencies and assumes only one right version

Lang specific solution - venv (for python), etc. But cross language environment support is a pain.

12 of 136

VMs solved, but with the tinkering overhead on u.

Containers solved the overhead of VMs resources and more. Containers are great❤️ but a bit overkill and heavy for scripting and other local workflows because of base image, reliance on conventional package manager and imperative dockerfile etc.

13 of 136

Let's have a very basic dockerfile example:

FROM ubuntu:latest

RUN apt-get update && \

apt-get install -y curl wget

WORKDIR /app

COPY . .

CMD ["curl" "--version"]

Dockerfile Problems:

  1. Repeatable, not reproducible
  2. LTS end version change
  3. App removed from repo
  4. Running same dockerfile after months or years will yield different result

14 of 136

Same Old Story, Different Surface:

Earlier: You made imperative steps(scripts) to set & patch things up in VMs.🙂

Now: You make imperative steps(dockerfile) to set & patch things up in Docker.🫠

Both creating the imperative cracks💔.

15 of 136

Good Old Meme

16 of 136

Are we doomed to continuously tinker stuffs and pray that it works?

17 of 136

Are we cursed to continuously sync dev,test and prod environments in CI/CD?

18 of 136

One Solution: DECLARATIVE

Instead of instructing how to achieve a result,

You precisely declare what you want 🎯.

19 of 136

Advantages:

Portable 🌍

Atomic 💥

Reproducible 🔄

Tracked Changes 👀

And what if still that’s not enough?

20 of 136

How to actually solve 🤔:

Imperative cracks? 💥🧱

Diverging environments? ↔️🌍

Repetitive script failures? 🔁❌📜

Inconsistent builds across systems? ≠⚙️🖥️

21 of 136

Arc 2: Nix Unleashed: The Declarative Revolution Begins

22 of 136

Nix - The Revolutionary Declarative system for Determinism, Reliability and Precise control.

23 of 136

Nix is popularly synonymous to these:

(PF = Purely Functional -> Declarative -> Atomic -> Reliable)

  1. Nix - The PF Programming Language, Build Tool
  2. Nixpkgs - PF Package Manager and repo
  3. NixOS - PF Linux Distro
  4. Specific Nix projects - PF [Shell, Containers, VMs, Custom ISOs etc] (Anything as Code)

Nix takes declarative a step ahead through purely functional approach.

24 of 136

Nix is a universal build bootstrapping tool, which brings a language agnostic way to build, cache and store artifacts.

Artifacts can be - Shells, packages, containers, VMs, Custom ISOs etc

Language Agnostic = Not tied to a single ecosystem like python, rust, etc

25 of 136

Nix Store : The Immutable Read Only Store

How it’s different from traditional PMs - Hashing, Symlinks.

  1. Hashing - Thus coexistence of any number of versions of same package🔥.

Eg:

/nix/store/b6gvzjyb2pg0kjfwrjmg1vfhh54ad73z-firefox-33.1

26 of 136

27 of 136

Live graph: https://repology.org/repositories/graphs

Nixpkgs search: https://search.nixos.org/packages

(120,000 packages while preparing this PPT🔥)

28 of 136

Guess the size of the whole nixpkgs repo? 🤔

�How many TBs or 100s of GBs?

Answer ->

29 of 136

It’s Just 5.05 GB🔥🔥🔥🔥🔥

Including all revisions and releases from the past

Because everything are just bootstrapped nix files, all text , with all revisions of older packages too. It’s super efficient.

nixpkgs use source repo model instead of binary repo model with optional hashed binary caching substituters. Eg: cache.nixos.org

30 of 136

Caching - Reusing the artifacts

  1. Public cache - cache.nixos.org, etc

  • Local cache are stored in Nix store /nix/store along with other derivations.

Derivations = Artifact produced by nix, could be anything from text, file, OS etc.

31 of 136

Find all packages here:

https://search.nixos.org/packages

32 of 136

That’s a simple bird’s-eye view of Nix so far. Nix solves stuffs in various layer.

And for this workshop let’s focus more on….

33 of 136

Arc 3: The Scripting Saga: Conquering the Chaos

34 of 136

Problems around modern scripting

To Attendees:

List out some problems/difficulties in scripting.

35 of 136

Question for u all🤔:

Why organisation restrict their employees from installing external packages in their work system?

36 of 136

Scripting hardships:

  1. “It works on my machine”
  2. Installing dependencies pollute the user space.
  3. Even when u install dependencies in a pre-Script phase, the post-Script phase cleaning process should be careful to avoid polluting the user space again.
  4. Imagine midway script failure during any of these phases 💀. Manual Rollbacks and tinkering becomes a maintenance overhead.

37 of 136

One Solution:

Create an image from a DockerFile💀, source the local directory as working directory inside it💀 and then mount a volume to persist the stateful changes💀.

☠️Still with all the mentioned problems with DockerFile.

38 of 136

Reproducible Scripts with Docker. Easy right?

🥲

39 of 136

Imagine doing that in CICD pipelines to sync dev, test & prod🙂

40 of 136

Nix Shells

41 of 136

Nix Shells

Temporarily expose the package from /nix/store to the environment.

Temporary Expose = Adding the package binaries to the PATH temporarily

A very neat and simple trick right?😎

Needless to say about the advantage of multi version coexistence of the same package of nixpkgs in the /nix/store.

42 of 136

Today we gonna learn few different types of usage of Nix shells. So that u can start using them directly in ur everyday workflows.

43 of 136

Nix shells usage types:

  1. Ad-Hoc Shells & One-shot method
  2. In a script file
  3. Scripting inside flakes(portable)

44 of 136

Arc 4: Nix Shells: Ad Hoc Powers Activated!

45 of 136

Nix Ad hoc shell environments:

Let’s do some simple commands:

echo “hello world” | cowsay

But cowsay doesn’t exist. So let’s enter a nix shell with the cowsay package,

nix-shell -p cowsay

Now try the command again:

echo “hello world” | cowsay

46 of 136

What’s happening?

  1. Nix built the packages mentioned and
  2. Nix created a temporary shell environment with the built packages’ binaries added to the PATH.

Now exit the shell by typing exit and try the hello world command again.

U can see that Nix removed the binaries from PATH without polluting user environment.

47 of 136

U can also enter a shell, run a command and exit in one shot🔥 like this:

nix-shell -p cowsay --run 'echo "hello, world" | cowsay'

nix-shell -p cowsay --command 'echo "hello, world" | cowsay'

48 of 136

  • --command cmd�In the environment of the derivation, run the shell command cmd. This command is executed in an interactive shell. (Use --run to use a non-interactive shell instead.) However, a call to exit is implicitly added to the command, so the shell will exit after running the command. To prevent this, add return at the end; e.g. --command "echo Hello; return" will print Hello and then drop you into the interactive shell. This can be useful for doing any additional initialisation.
  • --run cmd�Like --command, but executes the command in a non-interactive shell. This means (among other things) that if you hit Ctrl-C while the command is running, the shell exits.

49 of 136

And if u want to run the program directly without installing in the user environment. U can do this🔥🔥:

nix run nixpkgs#cowsay -- "hello, world"

Which is equivalent to the normal command:

cowsay "hello, world"

Everything after the “--“ are just usual flags and arguments usage.

50 of 136

Try it yourself:

Now make this command deterministic with Nix:

echo “hello, world” | cowsay | lolcat

Answer:

(next slide)

51 of 136

(All one liner commands)

Ans:

nix-shell -p cowsay lolcat --run ‘echo “hello, world” | cowsay | lolcat’

Bonus Tip (U can also pipe commands like this):

echo "hello, world" | nix run nixpkgs#cowsay | nix run nixpkgs#lolcat

52 of 136

Towards more reproducibility and determinism🔥💪:

nix-shell -p cowsay lolcat \

--pure \

-I nixpkgs=<NIXPKGS_REFERENCE> \

--run 'echo "hello, world" | cowsay | lolcat'

53 of 136

where <NIXPKGS_REFERENCE> can be:

channel:nixos-<NIXPKGS_CHANNEL>

(or)

https://github.com/NixOS/nixpkgs/archive/<COMMIT>.tar.gz

where <COMMIT> can be:

refs/heads/nixos-<NIXPKGS_CHANNEL>

(or)

dad564433178067be1fbdfcce23b546254b6d641

#use https://www.nixhub.io/ for finding the commit SHA for your package version

where <NIXPKGS_CHANNEL> can be:

25.05 (or) 25.11 (or) nixos-unstable etc...

54 of 136

Example:

nix-shell -p cowsay lolcat \

--pure \

-I nixpkgs=channel:nixos-25.05 \

--command 'echo "hello, world" | cowsay | lolcat'

Where nixpkgs can also be:

nixpkgs=channel:nixos-25.05

nixpkgs=https://github.com/NixOS/nixpkgs/archive/refs/heads/nixos-25.05.tar.gz

nixpkgs=https://github.com/NixOS/nixpkgs/archive/dad564433178067be1fbdfcce23b546254b6d641.tar.gz

55 of 136

Some flags:

--keep name�When a --pure shell is started, keep the listed environment variables.

--pure

If this flag is specified, the environment is almost entirely cleared before the interactive shell is started, so you get an environment that more closely corresponds to the “real” Nix build. A few variables, in particular HOME, USER and DISPLAY, are retained.

And explore lot more options and flags with:

nix-shell --help

56 of 136

Any Doubts so far🙋?

57 of 136

58 of 136

Let’s Continue🔥

59 of 136

Shell Scripting

60 of 136

Reproducible scripts using Nix:

Imagine a simple script like this:

#!/usr/bin/env bash

echo "Hello, myself!" | figlet | lolcat

echo "You're a great person!" | cowsay | lolcat

python --version

Qn) What is wrong with this script?👀 What if others run this?

61 of 136

One of the Ans) It assumes the system have installed the dependencies like python, lolcat, cowsay and figlet.

But what about something fundamentally wrong with this script?

Any Guess?

Ans: Next Slide

62 of 136

First of all, the script assumes the system already have bash installed 🥲

How can we improve this?

  1. Pre & Post Script installations? Nah

It’ll increase the risk of polluting the user space. Think python version override in pre script and again installing old version in postscript? What if the user installed the old one with some flags? It’s messier.

63 of 136

b) We can utilize nix shells we saw earlier? Okayish…. But Nah

Using nix-shell command in each line affects the readability and not easy to maintain. Eg: If we want to bump a version we might need to go all over the scripts and change the value. It’s not easy to maintain.

So how to solve all these?😞

64 of 136

Reproducible interpreted scripts with Nix

65 of 136

Reproducible interpreted scripts with Nix

#!/usr/bin/env nix-shell

#! nix-shell -i bash --pure --quiet

#! nix-shell -p bash cowsay figlet lolcat python312 nix

#! nix-shell -I nixpkgs=channel:nixos-25.05

echo "Hello, myself!" | figlet | lolcat

echo "You're a great person!" | cowsay | lolcat

It’s the same script with nothing changed in the body😁

But why there are multiple shebangs?🧐What's going on? Any Guess?

66 of 136

#!/usr/bin/env nix-shell

The first line is the usual shebang mentioning the interpreter to use which is nix-shell in this case.

#! nix-shell -i bash --pure --quiet

#! nix-shell -p bash cowsay figlet lolcat python312 nix

#! nix-shell -I nixpkgs=channel:nixos-25.05

And the following shebang like lines are interpreted by nix-shell.

Instead of giving out the nix-shell command with all flags in a lengthy format, for readability we are splitting it into multiple lines.

67 of 136

Are we nesting the shell with different flags? That will start unnecessary child processes and not efficient right? Haha

Actually the nix-shell interpreter combines all those flags and options and start a single environment instead of nesting. As u might guess the shebang like syntax is the signal for it to combine the flags and options.

Now with just adding these 3 more lines at the top without even disturbing the body we have just made this script 100% deterministic to work on any system.

See we didn’t even expect bash to be present, Nix is the only dependency we need. Super cool right?😁

68 of 136

Let’s break down the new flags:

#! nix-shell -i bash --pure --quiet

--quiet : Just avoid the verbose output while building the dependencies

-i bash : Chooses the interpreter to use for the of the script

69 of 136

Let’s extend the script body just a little:

#python from script

echo Your python version for this script is:

python --version

echo --------------------

70 of 136

Imagine: In the dependencies we mentioned python 312 and in between the script we want to use another particular python version for a a legacy program. Is that even possible?

At this point HOW should be the question. Let’s reuse that ad hoc shell idea here. It’s simple as that.

#another version of python as a nested nix shell

echo Your another python version is:

nix-shell -p python314 \

--pure --quiet \

-I nixpkgs=channel:nixos-25.05 \

--command 'python --version'

Now let’s look at the output of the script, just to verify.

71 of 136

72 of 136

So here just for a quick one time run for a program we literally used another python version.😁 U can literally use

  • any number of different versions of
  • any program together in
  • any script without conflict😎 and super reliable.

All with just only Nix as dependency🔥

73 of 136

Any Doubts so far🙋?

74 of 136

75 of 136

Want to Level Up

more?

76 of 136

Let’s do it 🔥

77 of 136

Scripting inside flakes(portable):-

The same scripting, but instead of in a separate .sh file we gonna do it inside a nix flake(just a config format).

78 of 136

Some advantages are:

  1. Easily shareable and when saved in a git repo, just a single command is needed to run your app on anyone’s machine.
  2. More precise control over the dependencies,
  3. Lock file,
  4. tight integration with nix ecosystem etc

Don't get overwhelmed. It’s easy and nix does the heavy lifting for you😉.

79 of 136

Arc 5: Unlocking the Functional Texts: Nix Language Fundamentals

80 of 136

Let’s learn some Nix language basics in just few mins.

It’s a super easy language.

(kind of like JSON with Functions😉)

81 of 136

integer = 4

float = 3.14

string = "nix"

multiLineString = ''

hello

Greetings my friend

''

paths = ./Documents

Data Types (Primitive)

82 of 136

string = "nix";

stringWithInterpolation = ''

hello ${string}

'';

String Interpolation(substitution):

83 of 136

# list values are separated by

#whitespaces(space or new line)

list = [hello 1 2.24]

#same list more readable

list1 = [

hello

1

2.24

]

Data Types (List)

84 of 136

# set/attributes values

#are separated by ;

set = {

name = "hello";

values = [1 2 3 ];

set2 = {a = 1; b = 2;};

}

Data Types (Set/ Attribute)

85 of 136

# Nested sets can be defined

#in a piecewise fashion.

{

a.b = 1;

a.c.d = 2;

a.c.e = 3;

}.a.c

#=> { d = 2; e = 3; }

86 of 136

Same thing when expanded:

#Nix

{

a = {

b = 1;

c = {

d = 2;

e = 3;

};

};

}

#JSON

{

"a": {

"b": 1,

"c": {

"d": 2,

"e": 3

}

}

}

87 of 136

let

x = "a";

in

{

name = x;

}

let in bindings

88 of 136

Nix have only lambda functions

# nix

x: x + 2

#python

lambda x: x + 2

#javascript

(x) => x + 2;

#java

(x) -> x + 2;

89 of 136

90 of 136

91 of 136

Nix functions have only one parameter

92 of 136

So we give a set as the parameter and access the values from it

93 of 136

<- Bad readability

Better Readability ->

94 of 136

Default values for the function

95 of 136

Both idea into one: (flexible)

Set and destructured set values

96 of 136

Any Doubts so far🙋?

97 of 136

98 of 136

Arc 6: The Flake Awakening: Configuration of Destiny

99 of 136

Nix flakes:-

Portable configuration(mostly boilerplate) to declare things precisely.

100 of 136

{

description = "flake";

inputs = {

nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";

flake-utils.url = "github:numtide/flake-utils";

};

outputs = { self, nixpkgs, flake-utils, ... }:

flake-utils.lib.eachDefaultSystem (system:

let

pkgs = import nixpkgs { inherit system; };

in {

packages.default = {

};

}

);

}

Boilerplate

(Don’t worry about the details for now)

101 of 136

Flakes is just a set with 3 parts;

  1. description: Type - String

Name of the flake configuration. (optional but good practice)

description = "flake";

  • inputs: Type - Set

We set all the inputs here

inputs = {

nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";

flake-utils.url = "github:numtide/flake-utils";

};

102 of 136

3) outputs: Type - Function

outputs = {}:

let

in {

}

outputs = { flake-utils, ... } @ inputs:

flake-utils.lib.eachDefaultSystem (system:

let

in {

}

);

103 of 136

outputs = { self, nixpkgs, flake-utils, ... } @ inputs:

flake-utils.lib.eachDefaultSystem (system:

let

pkgs = import nixpkgs { inherit system; };

shell-app = pkgs.writers.writeBashBin "mybashshellapp" {} '''' ;

in {

packages.default = shell-app;

}

);

Full outputs Boilerplate:

104 of 136

shell-app = pkgs.writers.writeBashBin

#app name

"mybashshellapp"

#extra arguments

{

makeWrapperArgs = [

"--prefix" "PATH" ":" "${pkgs.lib.makeBinPath [

#add dependencies here

]}"

];

}

#script without shebang

''

'' ;

Nix Writer function for Bash

3 arguments:

name: String

args: Set

script : String

105 of 136

#extra arguments

{

makeWrapperArgs = [

"--prefix" "PATH" ":" "${pkgs.lib.makeBinPath [

#add dependencies here

pkgs.cowsay

pkgs.lolcat

pkgs.figlet

pkgs.python312

pkgs.nix

]}"

];

}

Adding dependencies

106 of 136

107 of 136

Note this portion:

echo hello version is:

${pkgs.hello}/bin/hello --version

echo

echo again hello version is:

hello --version

The first interpolated version will work perfectly, but the second time hello is called it’ll throw an not found error. Guess what’s happening here?

108 of 136

Ans:

Interpolated method runs the binary only that time and doesn’t even expose the package to the script 🤯🔥

So it’s possible to run a different version of application without polluting not just the user space, but also not polluting even the script environment🔥🔥🔥

109 of 136

There are many such writers for other languages too with their own specific options, so feel free to explore scripting in your favourite language without worrying about dependencies management, version conflicts etc etc

Check out :

https://noogle.dev/q?term=pkgs.writers

110 of 136

At this point u can aura farm even with the Ant Veteran scripters.

111 of 136

Congratulations U have scratched the surface of Nix’s powers

🥳🎉

112 of 136

What we learnt today is how we can use Nix to supercharge scripting.

And it’ll help u understand further into the nix ecosystem where nix revolutionize things like:

113 of 136

114 of 136

Arc 7: Beyond the Horizon: The Vastness of the Nix Ecosystem

115 of 136

Development Shell Environments

116 of 136

App build

117 of 136

docker

(container)

image

118 of 136

NixOS

The declarative and functional OS which is atomic, reliable, deterministic, easy rollbacks etc.

119 of 136

120 of 136

121 of 136

122 of 136

123 of 136

124 of 136

125 of 136

126 of 136

127 of 136

128 of 136

129 of 136

130 of 136

131 of 136

132 of 136

NixOS generators

133 of 136

NixOS VMs

nixos-rebuild --flake .#vm build && \

./result/bin/run-vm

134 of 136

Ask your doubts

135 of 136

Thought of the day:

Hard work < Smart work < Right Work

Thank You All☺️

Vivekanandan KS

https://linksta.cc/@vivekanandanks

Feedback Form: (Next Slide)

136 of 136

Feedback Form: