Intro to Nixos for WSL
Table of Contents
- Setting up base instance of Nixos
- Nixos Flakes - The Basics
- Using
flake.nix
- Using local .nix files (Flakes)
- Next steps
If you are like me, you have multiple machines that you switch between to work on projects throughout the day. WSL is good at being portable as we saw in this post on Portable Workspaces with WSLv2.
However, sometimes we need a bit more portability. Nixos can give use a higher degree of flexibility when it comes to package management; especially across multiple distributed WSL instances.
Link to part two: Improving the Nixos Experience
Setting up base instance of Nixos
Official Nixos documentation for this section can be found here.
Install WSL (if applicable)
wsl --install --no-distribution
Download the Nixos Tarball
Link to the Tarball => nixos-wsl.tar.gz
Import image into WSL
wsl --import NixOS $env:USERPROFILE\wsl-images\nixos\ nixos-wsl.tar.gz
Once done, connect to the instance.
wsl -d NixOs
Nixos Flakes - The Basics
Now that you are in the instance, you will likely want some packages, maybe customize the user or hostname. To do this, we can use Nixos Flakes. These are portable configurations that can be applied to most installs of Nixos; WSL or not. Using flakes consists of two major files:
flake.nix
- Entry point for configuration
<hostname>.nix
- Any local .nix file(s) for your nix config
These files are used to compile a system configuration. Nix will then us this configuration to make changes to the existing build of the system. This includes adding/removing packages, changing host names, adding users, configuring 3rd party software packages, and much more. The options are limitless.
Using flake.nix
The flake.nix
file will act like an entry point for our Nixos builds. This file defines the repos and libraries that will be used by the build. Some examples would be nixpkgs
or nixos-wsl
. It also contains all the possible hosts that can be built from this config, and their respective packages and configurations.
In Nixos everything is a function. You’ll quickly notice that all of these config files are just lambda functions. Access to repos like nixpkgs
allows us to access more functions. These functions could define software packages, system configurations, or even WSL configurations (wsl.conf).
Now, lets set a baseline flake.nix
configuration for Nixos. In our WSL home directory, lets create a folder called config.
In this folder, lets create our flake.nix
file.
{
# These are the package dependency URLs.
inputs = {
# This is what your entire system is based on, it has every package and library function.
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
# The WSL specific modules.
nixos-wsl.url = "github:nix-community/NixOS-WSL/main";
};
# This will include modules that other flakes can use, or system configurations.
outputs = { self, nixpkgs, nixos-wsl, ... }: {
# This is for a system configuration(s)
nixosConfigurations = {
# Each of these blocks represents one system configuration.
# `hostName = ... {config options}`
nixdev = nixpkgs.lib.nixosSystem rec {
# Desired kernel
system = "x86_64-linux";
# These are the modules, or configs to use.
modules = [
# This includes the options for the WSL specific settings.
nixos-wsl.nixosModules.default
# External host configuration.
./nixdev.nix
];
};
};
};
}
Lets break down this code block.
# These are the package dependency URLs.
inputs = {
# This is what your entire system is based on, it has every package and library function.
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
# The WSL specific modules.
nixos-wsl.url = "github:nix-community/NixOS-WSL/main";
};
...
inputs
represent the packages and sources that will be used in our configuration. The inputs block brings these sources into scope for use in the following code and external packages. However, we will need to further provide these packages to other scopes as we go into external files. This block defines the default Nixos package repo nixpkgs
and an alternate repo for WSL specific configurations nixos-wsl
.
...
# This will include modules that other flakes can use, or system configurations.
outputs = { self, nixpkgs, nixos-wsl, ... }: {
# This is for a system configuration(s)
nixosConfigurations = {
...
outputs
represents the final configuration for the system. All configurations setup on this file, and the next few files, will be compiled into this object and used to generate our build. nixosConfigurations
will define our host specific configurations. You can define multiple configurations here to allow for maximum reusability.
...
nixdev = nixpkgs.lib.nixosSystem rec {
# Desired kernel
system = "x86_64-linux";
# These are the modules, or configs to use.
modules = [
# This includes the options for the WSL specific settings.
nixos-wsl.nixosModules.default
# External host configuration.
./nixdev.nix
];
};
...
nixdev
represents our host config. This is a friendly name, and will only be used when we try and use the flakes during rebuild. system
is the kernel we want for our host. Since we are not on ARM or Apple Silicon, we want this value to be x86_64-linux
. modules
represents the other .nix files we want to compile into our output. This can be a function call like nixos-wsl.nixosModules.default
or a local file like ./nixdev.nix
.
Using local .nix files (Flakes)
<hostname>.nix
Now that we have our entry point and host specific config defined in flake.nix
, we can define local .nix files that contain our modular configurations. These local .nix files are commonly referred to as flakes. While not required, I prefer to use a host specific .nix file, named <hostname>.nix
. Using this file, I will define the other local .nix files that I want to be a part of this specific host.
Here is an example of a flake configurations for my host called nixdev.nix
.
{ pkgs, lib, config, ... }: {
# What version to run on, this just says to run on whatever release you imported from nixpkgs.
system.stateVersion = config.system.nixos.release;
# This lets the system use flakes.
nix.settings.experimental-features = [ "nix-command" "flakes" ];
# (they are 'experimental' but everyone uses them, and they are great)
# Imports local modules
imports = [
./modules/generic-wsl.nix
];
# Change this and the config name in the flake.nix file to change the hostname.
networking.hostName = "nixdev";
}
Ill break this down into its components like I did for the flake.nix
file.
{ pkgs, lib, config, ... }: {
...
This defines the modules we want in-scope for our flake.
...
system.stateVersion = config.system.nixos.release;
...
This defines the version of Nixos we want to install.
...
nix.settings.experimental-features = [ "nix-command" "flakes" ];
...
Allows the use of Flakes on the host.
...
imports = [
./modules/generic-wsl.nix
];
...
These are the other flakes we want to include in this flake. Note that other flakes can define the same, creating a dependency chain. When nixos compiles the desired image, it will sort through this chain and ensure everything is included safely. In this case, I’m importing another local flake called generic-wsl.nix
.
...
networking.hostName = "nixdev";
...
This defines the hostname of the machine.
generic-wsl.nix
In my configuration, I use generic-wsl.nix
to setup all the general WSL configurations and include other flakes that install packages for more specific purposes.
{ pkgs, lib, config, ... }: {
imports = [
./docker.nix
./packages.nix
./jekyll.nix
./azdev.nix
];
# The WSL Specific settings from the 'nixos-wsl' module
wsl = {
enable = true;
defaultUser = "james";
docker-desktop.enable = false;
};
# user settings
users.users.james = {
# Says you can actually be the user, it is not a service only user.
isNormalUser = true;
# Puts you in the wheel group.
extraGroups = [ "wheel" "docker" ];
};
environment = {
# checks if rebuild is active on user logon
shellInit = ''
# prints rebuild.service's status if running ONLY
state=$(systemctl is-active rebuild)
[ "$state" == "active" ] && echo -e "Rebuild.Service (nixos-rebuild) \e[31m$state\e[0m"
'';
# Command aliases
shellAliases = {
nixos-refresh = ''state=$(systemctl is-active rebuild); [ "$state" != "active" ] && sudo ${lib.getExe pkgs.nixos-rebuild} switch --flake git+ssh://git@github.com/jimurrito/nixos-config#${config.networking.hostName} --refresh'';
nixos-status = ''state=$(systemctl is-active rebuild); color=$([ "$state" == "active" ] && echo "\e[31m" || echo "\e[32m"); echo -e "Rebuild.Service (nixos-rebuild) $color$state\e[0m"'';
};
};
# Systemd rebuild service for on-boot rebuids of nixos from the repo
systemd.services.rebuild = {
script = "${lib.getExe pkgs.nixos-rebuild} switch --flake git+ssh://git@github.com/<username>/nixos-config#${config.networking.hostName} --refresh";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [ git openssh ];
restartIfChanged = false;
};
}
There is a lot to go over here. For now, we will skip the configurations under environment
. That will be covered in a follow-up post on using Nixos in WSL.
imports = [
./docker.nix
./packages.nix
./jekyll.nix
./azdev.nix
];
...
Just like in the <hostname>.nix
file, these are the other local flakes we want to include when this flake is used. In this case, these are flakes that install packages for specific projects or tool chains.
...
wsl = {
enable = true;
defaultUser = "james";
docker-desktop.enable = false;
};
...
This defines the same configurations we would use in a regular wsl.conf
file.
...
users.users.james = {
isNormalUser = true;
extraGroups = [ "wheel" "docker" ];
};
...
This defines our user, and the groups they should be apart of.
If we wanted to add or install some packages, we could add a line like this.
...
environment.systemPackages = with pkgs; [ jekyll bundler bundix ];
...
This would install the packages needed to develop Jekyll projects in nixos. If you are familiar with Jekyll, you’ll notice the newcomer bundix
. This is used to allow for building the bundle
packages within a virtual Nixos shell. We will not be covering this tool in this post. However, I do plan on posting a follow-up to the post on setting up Jekyll for local use.
With all of this set, we can now apply our configuration into our build of Nixos. Running this below command will use our local flakes to build our image live.
nixos-rebuild switch --flake ./flake.nix#hostname
Note:
hostname
should be replaced with your desired host config we setup in theflake.nix
file. For this demo, my host config is callednixdev
. If you do not provide a hostname, it will default to the current hostname of the machine.
nixos-rebuild switch --flake ./flake.nix#nixdev
Once done, thats it! If this operation was successful, we should have all the packages needed. Some changes like changing the hostname of a machine does require a reboot to propagate.
Next steps
From here, you should be able to customize Nixos to your hearts content. In the next post, I will go over some more advanced configurations and QOL changes I added to my Nixos installs. Link to part two: Improving the Nixos Experience
Helpful links
Written by James Immer