This is a guide to my personal dotfiles repo (for NixOS and Darwin systems), use it as a reference to create your own.
Installation #
I suggest anyone that wants to try out Nix not to use the official installer, but to use the Nix Installer by Determinate Systems. It installs Nix, while providing a way to easily remove it from your system.
1curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install --no-confirm
Flakes? #
If you are following online guides, a lot of those will probably tell you to run
some commands start with nix-env
or nix-channel
, don't use it! Those nix-
commands are channel based, while
"reproducible", but hard to maintain. Instead, use
Nix Flakes, think of it as a way to pin
channels to a specific commit, so you can get the same result every time if the
same flake inputs are given.
While Nix Flakes are still marked as "experimental", but it does not mean it's unstable. Members of the Nix community are already using flakes at scale, I don't really see any way of flakes being removed from Nix.
If you installed Nix using the DetSys Nix Installer, you should already have flakes
enabled. If not, prefix all nix
commands with
nix --extra-experimental-features "nix-command flakes"
. If you see something
like
echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
in
a guide, don't do it either. Instead, in your flake configurations, enable
experimental features like this (in the module format):
1{
2 nix.settings.experimental-features = [
3 "flakes"
4 "nix-command"
5 ];
6}
Read more about flakes:
Don't want to use Flakes? #
If you don't want to use flakes, there are still ways to pin your dependencies
to a specific commit (the use of channels is not recommended). You can use
niv
or npins
to pin your dependencies.
While niv
and npin
are good tools, IMO they are not as good as flakes.
You can achieve similar results with niv
and npin
, but you'll have to
install more tools and (potentially) write more code.
Similar effects can also be achieved with flake-compat
,
but you are still using flakes, just in a different way.
Basics #
Update (2024-02-01):
Before diving into system configurations, you should understand the following concepts:
- derivations and closures
- nix store
- using nix as a package manager
- development shell
I prepared a short slide with examples to help you understand these concepts, you can find it here and more resources at the end of this guide.
Modules #
In the example above, flakes
and nix-command
are enabled in the format of a
module. Modules can be imported into system configurations, and they are usually
considered as the most basic building blocks of nix-based configurations. They
can be attrsets, or a function that returns an attrset.
Generally, modules are imported directly from flake outputs inside a system configuration:
1{
2 inputs = {
3 nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
4 nix-darwin = {
5 url = "github:lnl7/nix-darwin";
6 inputs.nixpkgs.follows = "nixpkgs";
7 };
8 };
9
10 outputs = { self, nixpkgs, nix-darwin } @ inputs:
11 let
12 inherit (self) outputs;
13 lib = nixpkgs.lib // nix-darwin.lib;
14 in
15 {
16 nixosConfigurations."nixos-machine-name" = lib.nixosSystem {
17 specialArgs = { inherit inputs outputs; };
18 modules = [
19 ... # put modules here
20 ];
21 };
22 darwinConfigurations."darwin-machine-name" = lib.darwinSystem {
23 specialArgs = { inherit inputs outputs; };
24 modules = [
25 ... # put modules here
26 ];
27 };
28 };
29}
The specialArgs
is used to pass arguments to modules, we added inputs
and
outputs
to it so we can use them in modules like this:
1{ inputs, outputs, ... }:
2
3{
4 ... # inputs and outputs are available here
5 # example: inputs.nixpkgs or inputs.nix-darwin
6 # example: outputs.nixosConfigurations."nixos-machine-name"
7 # be very careful when using outputs, it can cause infinite recursion
8}
Standard arguments like config
, pkgs
,
modulePath
, ... are passed to modules automatically.
Getting Started on Configurations #
Depending on whether you have a NixOS or Darwin system (or both), you should decide on these couple of things:
- How many host machines do you have? Is it really worth it spending time on Nix?
- NixOS or Darwin? Or both (you've got to think it through if you want to have both, it'll be quite messy)?
- Do you want to use
home-manager (considering
nixpkgs
andnix-darwin
both have limited configuration options, I personally strongly recommend using home-manager)? - Standalone home-manager or integrate it directly into your system
configuration (standalone meaning you'll need to run
home-manager switch
instead of it automatically kicks in when rebuilding the system with{nixos,darwin}-switch
)?
Let's continue with the assumption that you have multiple NixOS and Darwin machines, and you want to use home-manager integrated into your system configuration. The first step would be adding flake inputs:
1{
2 inputs = {
3 nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
4 nix-darwin = {
5 url = "github:lnl7/nix-darwin";
6 inputs.nixpkgs.follows = "nixpkgs";
7 };
8 home-manager = {
9 url = "github:nix-community/home-manager";
10 inputs.nixpkgs.follows = "nixpkgs";
11 };
12 };
13
14 outputs = { self, nixpkgs, nix-darwin, home-manager, ... } @ inputs:
15 let
16 inherit (self) outputs;
17 lib = nixpkgs.lib // nix-darwin.lib // home-manager.lib; # merge all libs together so we don't need to use them separately
18 in
19 {
20 nixosConfigurations."nixos-machine-name" = lib.nixosSystem {
21 specialArgs = { inherit inputs outputs; };
22 modules = [
23 ... # put modules here
24 ];
25 };
26 darwinConfigurations."darwin-machine-name" = lib.darwinSystem {
27 specialArgs = { inherit inputs outputs; };
28 modules = [
29 ... # put modules here
30 ];
31 };
32 };
33}
This is rather cumbersome, let's extract the common section out:
1{
2 inputs = { ... }; # same as above
3
4 outputs = { self, nixpkgs, nix-darwin, home-manager, ... } @ inputs:
5 let
6 inherit (self) outputs;
7 lib = nixpkgs.lib // nix-darwin.lib // home-manager.lib; # merge all libs together so we don't need to use them separately
8 mkSystem = { type, platform, config, username, stateVersion, hmstateVersion, extraModules, extraHMModules }: lib."${type}System" {
9 specialArgs = { inherit inputs outputs; };
10 modules = [
11 config # path to system config, hardware configs are usually imported inside the config
12 { nixpkgs.hostPlatform = lib.mkDefault platform; } # set host platform
13 { system.stateVersion = stateVersion; } # state version
14 { home-manager.users."${username}".home.stateVersion = hmstateVersion; } # home-manager state version
15
16 # system user
17 (../. + "/users/${username}") # or change it based on your directory structure
18
19 # integrated home-manager
20 home-manager."${type}Modules".home-manager
21 {
22 home-manager.extraSpecialArgs = { inherit inputs outputs; };
23 home-manager.useGlobalPkgs = true;
24 home-manager.useUserPackages = true;
25 home-manager.users."${username}" = {
26 imports = [
27 (../. + "/users/${username}/home.nix") # or change it based on your directory structure
28 ] ++ extraHMModules;
29 };
30 }
31 ] ++ extraModules;
32 };
33 in
34 {
35 # rec means recursive attrset, attrs inside recursive attrset can refer to other attrs inside the scope
36 nixosConfigurations."nixos-machine-name" = mkSystem rec { # we are using rec here since home-manager state version is the same as nixos state version
37 type = "nixos";
38 platform = "x86_64-linux"; # or "aarch64-linux"
39 config = ./path/to/nixos/configuration.nix;
40 username = "your-username";
41 stateVersion = "24.05"; # string type
42 hmstateVersion = stateVersion; # string type
43 extraModules = [
44 # extra nixos modules
45 ];
46 extraHMModules = [
47 # extra home-manager modules
48 ];
49 };
50 # we don't need to use rec here since we are not referring to other attrs
51 darwinConfigurations."darwin-machine-name" = mkSystem {
52 type = "darwin";
53 platform = "x86_64-darwin"; # or "aarch64-darwin"
54 config = ./path/to/darwin/configuration.nix;
55 username = "your-username";
56 stateVersion = 4; # integer type
57 hmstateVersion = "24.05"; # string type
58 extraModules = [
59 # extra darwin modules
60 ];
61 extraHMModules = [
62 # extra home-manager modules
63 ];
64 };
65 };
66}
mkSystem
is a function that returns a system config, it assumes you have the
following directory structure:
1- flake.nix
2- flake.lock
3- some-other-directory-that-stores-your-system-config
4 - ...
5- some-other-directory-that-stores-your-modules
6 - ...
7- users
8 - your-username
9 - home.nix # home-manager module
10 - default.nix # nixos/darwin module
You can change the directory structure, but you'll need to change the paths in
mkSystem
accordingly. Note that users/${username}/default.nix
is a
nixos/darwin module, it's content must match the definitions in
nixpkgs or
nix-darwin.
users/${username}/home.nix
is a home-manager module, it's content must match
the definitions in
home-manager. Similarly, the
modules in extraModules
must be from
nixpkgs or
nix-darwin, and the modules in
extraHMModules
must be from
home-manager. Putting the mkSystem
function in a separate file is also a good idea, check out
Haumea to easily manage your custom
libraries.
Conflicts? System-Dependent Configs? #
Some options might only be available in nixpkgs options but not in nix-darwin
options, or vice versa. To address this,
lib.optionalAttrs
can be very useful:
1{ config
2, lib
3, pkgs
4, ...
5}:
6
7{
8 programs.zsh.enable = true;
9
10 users.users."your-username" = {
11 shell = pkgs.zsh;
12
13 description = "Your Username";
14 home =
15 if pkgs.stdenv.isLinux
16 then lib.mkDefault "/home/your-username"
17 else if pkgs.stdenv.isDarwin
18 then lib.mkDefault "/Users/your-username"
19 else abort "Unsupported OS";
20
21 openssh.authorizedKeys.keys = [
22 "ssh-ed25519 ...";
23 "ssh-ed25519 ...";
24 ];
25 } // lib.optionalAttrs pkgs.stdenv.isLinux {
26 isNormalUser = true;
27 extraGroups = [ "wheel" "networkmanager" "input" "audio" "video" ];
28 hashedPassword = "...";
29 };
30}
In the example above, users.users.<username>.isNormalUser
is only available in
nixpkgs (for NixOS systems, not Darwin), so we use
lib.optionalAttrs pkgs.stdenv.isLinux
to make it only available on Linux
systems or nix-darwin will throw an error.
Also, if you only want some packages to be installed on a specific system,
lib.optionals
can make attributes appear or disappear based on conditions:
1{ config
2, lib
3, pkgs
4, ...
5}:
6
7{
8 # available on all systems
9 home.packages = with pkgs; [
10 nix-output-monitor
11 # ...
12 ]
13 # linux only and when hyprland is enabled
14 ++ (lib.optionals (pkgs.stdenv.isLinux && config.wayland.windowManager.hyprland.enable) [
15 cider
16 # ...
17 ])
18 # darwin only
19 ++ (lib.optionals pkgs.stdenv.isDarwin [
20 cocoapods
21 # ...
22 ]);
23}
Resources #
Nix's documentation is bad, my best advise is get used to reading the source
code, and messing around with it using nix repl
. Instead of complaining about
the documentation, use online resources like
official discourse and
github code search (query with
lang:Nix
).
- zero to nix
- nix wiki: usually outdated, but still useful
- nix pills
- nix manual
- nixpkgs manual
- mynixos: a very nice tool to search for options
- direnv: a tool to automatically load/unload environment variables
- ... the rest is up to your imagination