⟰ Some NixOS Notes
⟸ The Nix Expression Language
⟹ Packages and Derivations
A module is a particular kind of Nix expression which
It’s a set, or a function that returns a set, that can look like this,
{ config, options, pkgs, lib, modulesPath, ... }:
{
imports = [
# import paths
];
options = {
# option declarations
};
config = {
# option definitions
};
}
or like this,
{ pkgs, ... }:
{
imports = [
# import paths
];
# option definitions
}
or maybe even just like this.
{
# option definitions
}
You can import paths to modules themselves, or you can import paths to directories. If you import a directory, the module named default.nix
inside it will be imported.
If the module is a function, it’s passed a set with the attributes
config
, the set of all option definitions for the system,options
, the set of all option declarations for the system,pkgs
, the set of all packages from <
nixpkgs/pkgs
>
,lib
, the set of utility functions provided in <
nixpkgs/lib
>
(which are documented in Nixpkgs Manual: 5.1. Nixpkgs Library Functions),modulesPath
, the path <
nixpkgs/nixos/modules
>
.configuration.nix
by ExampleYour configuration.nix
is a module; all it does is define some of the options declared by modules elsewhere. If you’d like, you can see for yourself how those modules are implemented in nixpkgs
. You can also search for options, their descriptions, and the files they’re declared in at Search: Options.
What follows are some example configurations to hopefully give you some helpful context for how these configurations can be used.
{ pkgs, ... }: {
imports = [
./hardware-configuration.nix
];
boot.loader.grub = {
enable = true;
version = 2;
}
users = {
# this disallows users and groups from being modified outside
# of the system configuration, which is useful if you only
# ever want to configure users in your `configuration.nix`
mutableUsers = false;
users = {
raccoon = {
# this is a regular interactive user with a home directory etc.
isNormalUser = true;
# and they can sudo
extraGroups = [ "wheel" ];
#! you can generate a hash for your password here
#! with the command "mkpasswd -m sha-512"
hashedPassword = "/*snip*/";
#! note the `with pkgs;`, so you don't have
#! to type `pkgs.vim` etc. for each package
packages = with pkgs; [ vim ];
}
}
}
networking = {
# even if you're using DHCP, it's not recommended to enable it globally
useDHCP = false;
#! interface name just an example; your `configuration.nix`
#! probably auto-generated with the name of yours
interfaces.enp1s0.useDHCP = true;
}
services.openssh.enable = true;
services.xserver = {
enable = true;
desktopManager.gnome3.enable = true;
# NixOS's default display manager is LightDM, GDM
# however fits better aesthetically with GNOME
displayManager.gdm.enable = true;
}
system.stateVersion = "20.09";
}
This configuration will give you
raccoon
who has access to vim
and can use sudo
commands,Note that NixOS has determined some things about your hardware and put its recommendations into hardware-configuration.nix
. The default configuration chooses to import this file because it includes some important information like what filesystem mounts and what kernel modules your system needs.
However, hardware-configuration.nix
is not special, and we could do all this ourselves! Let’s look at another example.
This is a configuration for a webserver that serves static files from /srv/www/<subdomain>
. It’s in two parts, and this part will be a demonstration of writing all the boring hardware-configuration.nix
stuff yourself, just for illustration.
If you want to skip to the next part, where we configure the actual webserver part and also write a little function to help us, click here. It’s got some interesting details even if you don’t care about configuring a webserver.
{
imports = [
./boot.nix
./fs.nix
./net.nix
./users.nix
#! the `web` directory will be defined in part 2.
./web
]
services.openssh.enable = true;
system.stateVersion = "20.09";
}
{ modulesPath, ... }: {
imports = [
# some helpful defaults for a virtualised environment like a VPS:
# <nixpkgs/nixos/modules/profiles/qemu-guest.nix>
(modulesPath + "/profiles/qemu-guest.nix")
];
boot = {
# many VPSes need a serial console for the service's builtin web console
kernelParams = [ "console=ttyS0" ]
# for SATA and SCSI
initrd.availableKernelModules = [ "ahci" "sd_mod" ]
loader.grub = {
enable = true;
version = 2;
# configure the menu but don't auto-install grub
device = "nodev";
# put kernels in /boot
copyKernels = true;
# partition labels are sometimes more reliable
# than drive UUIDs in virtualised environments
fsIdentifier = "label";
extraConfig = "serial; terminal_input serial; terminal_output serial";
}
};
}
{
fileSystems = {
"/" = {
device = "/dev/disk/by-label/nixos";
fsType = "ext4";
};
"/srv" = {
device = "/dev/disk/by-label/srv";
fsType = "ext4";
}
}
swapDevices = [
{
device = "/dev/disk/by-label/swap";
}
]
}
{
networking = {
useDHCP = false;
# the physical location of the same network interface can
# change in virtual environments managed by third parties.
# use incremental names like `ethX` instead of `enpXsY`
usePredictableInterfaceNames = false;
}
}
{
users = {
mutableUsers = false;
users = {
admin = {
isNormalUser = true;
hashedPassword = "/*snip*/";
extraGroups = [ "wheel" ];
# ssh auto-login with a public key
openssh.authorizedKeys.keys = [
"/*snip*/"
]
}
}
}
}
In this part we’ll set up nginx to use TLS and host two subdomains: www
and files
. We’ll write a function to help us configure both subdomains the same way without repeating ourselves.
I’ve annotated this part with notes that you might find interesting even if you don’t need to know about configuring nginx. Be on the lookout for highlighted #!
comments!
{
# import each subdomain
imports = [
./www.nix
./files.nix
];
# allow forwarding HTTP for our website
networking.firewall.allowedTCPPorts = [ 80 443 ];
# use ACME to get certificates for TLS (with Let's Encrypt as the default CA)
security.acme = {
acceptTerms = true;
email = "me@example.com"
};
services.nginx = {
enable = true;
# by default, nginx will restart instead of
# reload when its configuration is changed
enableReload = true;
# some relatively sane defaults you can check in the nixpkgs repo:
# <nixpkgs/nixos/modules/services/web-servers/nginx/default.nix>
recommendedOptimisation = true;
recommendedTlsSettings = true;
recommendedGzipSettings = true;
recommendedProxySettings = true;
};
}
#! here we're going to make a function that helps us not repeat
#! ourselves when we want to add a subdomain to our website.
#! it'll return some values we can plug into `services.nginx.virtualHosts`
sub: let
# define our domain and where we want to serve files from
domain = "example.com";
root = /srv/www;
#! notice how we nest `let ... in`, it's helpful for temporary values.
#! we need `domain` and `root` to define `subdomain`
#! and `subroot`, but we don't need it elsewhere
in let
# if we're given a subdomain, apply it to the domain and root directory
subdomain = if sub == null
then domain
else "${sub}.${domain}";
subroot = if sub == null
then root
else "${root}/${sub}";
#! we're using a `rec` set because we reference
#! `name` and `settings` in `definition`
in rec {
name = subdomain;
settings = {
forceSSL = true;
enableACME = true;
root = "${subroot}";
# return file named like the URI, directory named like the URI, or 404
locations."/".tryFiles = "$uri $uri/ =404";
};
# put attributes into a set that mimics the format
# of `services.nginx.virtualHosts` for convenience
definition = { ${name} = settings; };
}
let
#! we `import` the file to turn its contents into an expression
vhost = import ./vhost.nix;
in let
root = vhost null;
www = vhost "www";
in {
#! here we're merging some sets together:
#!
#! - we need change the default for `locations."/".return` given
#! by `vhost` because we just want `example.com` to redirect
#! to `www.example.com` instead of serve files
#!
#! - we also need to add the definition for `www.example.com`
#! to `services.nginx.virtualHosts` afterward
#!
services.nginx.virtualHosts = {
"${root.name}" = root.settings // {
locations."/".return = "301 https://${www.name}$request_uri";
};
} // www.definition;
}
#! this file barely contains anything! it just adds an
#! extra subdomain, and we made a function for that
let
vhost = import ./vhost.nix;
in {
services.nginx.virtualHosts = (vhost "files").definition;
}
Recall that all of these options we’ve been defining have been declared by some other module in nixpkgs
. In this section, we’ll look at the module that declares an option to provide a session for the i3
window manager.
{ config, lib, pkgs, ... }: with lib; let
cfg = config.services.xserver.windowManager.i3;
in {
options = {
services.xserver.windowManager.i3 = {
#! mkEnableOption is a helper function for generating an option that
#! accepts a bool and has the description "Whether to enable {name}":
#! <nixpkgs/lib/options.nix>#85
enable = mkEnableOption "i3 window manager";
#! mkOption is more general, it accepts a whole set of options:
#! nixpkgs/lib/options.nix#50
#! in this case, we set a default value, a description,
#! and a <nixpkgs/lib/types> type annotation
configFile = mkOption {
default = null;
type = with types; nullOr path;
description = ''
Path to the i3 configuration file.
If left at the default value, $HOME/.i3/config will be used.
'';
};
extraSessionCommands = mkOption {
default = "";
type = types.lines;
description = ''
Shell commands executed just before i3 is started.
'';
}
#! sometimes packages have multiple variants
#! you can use in place of the original.
#! in these cases, an option `package` is usually supplied to configure
#! what the module puts in `environment.systemPackages`
package = mkOption {
type = types.package;
default = pkgs.i3;
defaultText = "pkgs.i3";
example = "pkgs.i3-gaps";
description = ''
i3 package to use.
'';
};
extraPackages = mkOption {
type = with types; listOf package;
default = with pkgs; [ dmenu i3status i3lock];
example = literalExample ''
with pkgs; [
dmenu
i3status
i3lock
]
'';
description = ''
Extra packages to be installed system wide.
'';
}
};
};
#! mkIf just requires its argument to be true for the config to take effect
config = mkIf cfg.enable {
services.xserver.windowManager.session = [{
name = "i3";
start = ''
${cfg.extraSessionCommands}
${cfg.package}/bin/i3 ${optionalString (cfg.configFile != null)
"-c /etc/i3/config"
} &
waitPID=$!
'';
}];
environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages;
};
}
As you can see, it’s pretty simple. If you enable it, you get the i3
package and you get an X session that runs it.
This particular module is
<
nixpkgs/nixos/modules
/services/x11/window-managers/default.nix>
,<
nixpkgs/nixos/modules
/services/x11/xserver.nix>
,<
nixpkgs/nixos/modules
/module-list.nix>
,<
nixpkgs/nixos/lib
/eval-config.nix>
(to evaluate the config),<
nixpkgs/nixos
/default.nix>
.Like many modules, it uses a good amount of utility functions in <
nixpkgs/lib
>
. You can search for their definitions there or learn by looking at more examples of modules.
⟰ Some NixOS Notes
⟸ The Nix Expression Language
⟹ Packages and Derivations