Getting Started With NixOS

Posted

This post aims to be a quick tutorial to getting started managing systems with NixOS and NixOps.

I'm not going to cover actually getting a NixOS server running as NixOS has all of this information on their download page. So get a server running where you can SSH in as root then continue reading.

You will also need to install Nix on your local machine, this is because all of the hard work is done on your machine, then the results are sent to the target servers. (Logically that is. The data transfer between the two is actually pretty small.) Read the installation instructions to get started.

Basics

I'm going to start simple, a machine running NGINX. I'm not even going to upload a website to start, I'll just use the Valgrind docs (as the NixOps docs do). We will then work on evolving this to learn about keeping your configs tidy.

{pkgs, ...}: {
	# Describe your "deployment"
	network.description = "Web server";
	
	# A single machine description.
	webserver = {
		deployment.targetEnv = "virtualbox";
		deployment.virtualbox.memorySize = 1024; # megabytes
		
		services.nginx.enable = true;
		services.nginx.config = ''
			http {
				include       ${pkgs.nginx}/conf/mime.types;
				default_type  application/octet-stream;
				
				server {
					listen 80 default_server;
					server_name _;
					
					root "${pkgs.valgrind}/share/doc/valgrind/html";
				}
			}
		'';
	};
}

This example builds a VM but if you want to deploy over SSH just change the deployment.* lines to the following, filling in your IP and hostname.

deployment.targetEnv = "none";
deployment.targetHost = "x.x.x.x";
networking.hostName = "yourhost";
	
fileSystems."/" = {
	device = "/dev/disk/by-label/nixos";
	fsType = "ext4";
};

boot.loader.grub = {
	enable = true;
	version = 2;
	device = "/dev/sda";
};

Now you use NixOps to deploy.

nix-env -i nixops # Install NixOps
nixops create -d yourname path/to/yourconfig.nix # Create the deployment
nixops deploy -d yourname # Deploy!

The first two steps only need to be performed once, the last command can be run from any directory to deploy your servers.

Nix syntax

I'm not going to describe the syntax too much as it is explained in the manual. But I'll briefly touch on the basics.

When you are writing a configuration you are creating a nested set (that's what Nix calls it but I would consider it a dictionary). The Nix language has a lot of convenience syntax around sets for this reason. The most important thing to realize is that nested sets are automatically created.

a.b.c.d = 5
# Is the same as
a = { b = { c = { d = 5; }; }; }

So let's use that to simplify our config a bit.

{pkgs, ...}: {
	# Describe your "deployment"
	network.description = "Web server";
	
	# A single machine description.
	webserver = {
		deployment = {
			targetEnv = "virtualbox";
			virtualbox.memorySize = 1024; # megabytes
		};
		
		services.nginx = {
			enable = true;
			config = ''
				http {
					include       ${pkgs.nginx}/conf/mime.types;
					default_type  application/octet-stream;
					
					server {
						listen 80 default_server;
						server_name _;
						
						root "${pkgs.valgrind}/share/doc/valgrind/html";
					}
				}
			'';
		};
	};
}

That is a bit less repetition, and easier to read.

Modules

I like to keep my different services in modules so lets split out NGINX from our machine declaration. In my setup my module are divided into a number of modules based on how they are used. I'll describe my structure below but it is just my convention and you can call the directories whatever you want.

So to fit into this structure we'll extract the NGINX config into services/nginx.nix.

{pkgs, ...}: let
	# Extract the config into a binding.
	config = ''
		http {
			include       ${pkgs.nginx}/conf/mime.types;
			default_type  application/octet-stream;
			
			server {
				listen 80 default_server;
				server_name _;
				
				root "${pkgs.valgrind}/share/doc/valgrind/html";
			}
		}
	'';
in {
	services.nginx = {
		enable = true;
		config = config;
	};
}

And modify your existing config file to.

{pkgs, ...}: {
	# Describe your "deployment"
	network.description = "Web server";
	
	# A single machine description.
	webserver = {
		imports = [
			services/nginx.nix
		];
		
		deployment = {
			targetEnv = "virtualbox";
			virtualbox.memorySize = 1024; # megabytes
		};
	};
}

The way imports work is Nix is very nice, the file gets loaded and if it is a function is gets passed the global argument set (this is where we get the pkgs variable from). Then the result is merged into the importing module. This means that you don't have to worry about where in the tree your code gets included, it is always the root.

Now by commenting out or deleting the NGINX import and deploying you can simply remove the service from your system. This is the primary benefit of how I structure my code, selecting what services should be on a machine (other then the base services which are always present) is as simple as adding or removing imports from the services/ directory.

Further Reading

I set up NGINX because it is simple to start yet can quickly build in complexity as you start adding sites and SSL. I will be going through these in future posts to show some configuration patterns in Nix.

In the meantime the Nix docs include a complete list of options for NixOS as well as a complete list of available packages so you can get started on your own.

If you have any questions feel free to comment below but keep in mind that this tutorial wasn't meant to replace the Nix manuals, so look there first! However I do understand that the docs aren't perfect so if you are still stuck please ask in the comments.