Gabriel Cachadiña

My PC Configuration Using NixOS

· Gabriel Cachadiña

Recently I decided that, since I’m currently migrating my servers to NixOS, why not migrate my work machines to this distribution as well? At the moment I use three machines running Arch Linux, and to update/maintain them I use Ansible scripts. This works correctly for me, since Ansible allows me to create an endless number of modules to configure the programs I use, the services I run, the ports I expose, etc., in an idempotent way.

The only issue I’ve found with this workflow is that if, for example, I enable a port in an Ansible module and for whatever reason later want that port to no longer be enabled, I have to define a second module that closes the port. In other words, even if the module that enables the port does not run, that does not mean the port will be closed.

A possible solution to this is NixOS, which has a configuration file that defines absolutely everything. That way, if you want to open a port, you define it in the configuration file, but if that port is no longer needed and you remove it from the configuration, the port will be closed automatically.

Other benefits I would gain by migrating to NixOS include:

Installation

For the installation I decided to use the graphical NixOS installer and the GNOME desktop environment.

Removing Programs

As I mentioned before, I selected the GNOME desktop, but the default GNOME configuration on NixOS includes a large number of programs that I either won’t use or prefer to replace with alternatives. That’s why I define the removal of packages as follows:

1  # Remove the Bloat
2  environment.gnome.excludePackages = with pkgs; [
3    baobab
4    cheese
5    ...
6  ]

Global Variables

Throughout my system configuration I may want to use the same value many times. Since I might want to change that value in the future, I prefer to define it as a variable so that, if I ever change it, the change is replicated across all my configuration files. To achieve this, I define a variables file called globals.nix1 with the following structure:

 1# ./modules/globals.nix
 2{ lib, config, ... }:
 3
 4{
 5  options.globals.username = lib.mkOption {
 6    type = lib.types.str;
 7    description = "Primary user name";
 8    default = "gabriel";
 9  };
10}

With this, I have defined my username as a variable (in this example), so that if I want to use it in another file I no longer need to write:

1  users.users.gabriel = {
2    isNormalUser = true;
3    extraGroups = [ "networkmanager" "wheel" ];
4  };

Instead, I can use:

1  users.users.${config.globals.username} = {
2    isNormalUser = true;
3    extraGroups = [ "networkmanager" "wheel" ];
4  };

Bootloader

Configuration used during system startup, stored in modules/boot.nix.

Services

I define services as those programs or processes that I want to run in the background, such as cron jobs, programs like Syncthing, or programs that run when the machine starts or shuts down. These are defined in modules/services.nix. An excerpt from this file would be:

{ config, pkgs, ... }:
let
  SaveNixOSConfig = pkgs.writeShellScript "SaveNixOSConfig" ''
     rsync -av --no-owner --no-group --delete /etc/nixos/ /home/${config.globals.username}/Sync/NixOS/${config.globals.syncnixos}/
  '';
in
{
  ...

  services.cron = {
    enable = true;
    systemCronJobs = [
      "0 * * * *      ${config.globals.username}    ${SaveNixOSConfig}"
    ];
  };
}

Docker

Just like services, here I define all the Docker containers that will run on my machine. An example of this file would be:

 1{ config, pkgs, ... }:
 2
 3{
 4  virtualisation.docker.enable = true;
 5
 6  virtualisation.oci-containers = {
 7    backend = "docker";
 8
 9    containers.qbittorrent = {
10      image = "lscr.io/linuxserver/qbittorrent:latest";
11
12      environment = {
13        PUID = "1000";
14        PGID = "1000";
15        TZ = "Europe/Madrid";
16        WEBUI_PORT = "8080";
17        TORRENTING_PORT = "6881";
18      };
19
20      volumes = [
21        "/home/${config.globals.username}/Docker/qbittorrent:/config"
22        "/home/${config.globals.username}/Docker/Downloads:/downloads"
23      ];
24
25      ports = [
26        "127.0.0.1:8080:8080"
27      ];
28
29      # Equivalent to `restart: unless-stopped`
30      autoStart = true;
31    };
32  };
33}

Programs

For programs on my NixOS system, I split them into two main groups.

Basic Programs

Stored in modules/programs/programs.nix, this file contains all programs that do not require additional configuration. For example, the version of cmus I use has no extra configuration, so it would go here.

1{ config, pkgs, ... }:
2{
3  nixpkgs.config.allowUnfree = true;
4  environment.systemPackages = with pkgs; [
5     # Gnome Extensions
6     gnomeExtensions.blur-my-shell
7     gnomeExtensions.vitals
8     ...
9}

Customized Programs

Stored in the modules/programs/ directory and named after the program they configure. These files define not only the installation of the program itself, but also the dotfiles or additional configuration the program requires. For example, for my mpv configuration I use the following setup:

{ config, pkgs, ... }:

let
  mpvConf = pkgs.writeText "mpv.conf" ''
    fullscreen
    no-osd-bar
  '';
in
{
  environment.systemPackages = with pkgs; [
     mpv
  ];

  # DotFiles
  systemd.tmpfiles.rules = [
    "d /home/${config.globals.username}/.config/mpv 0755 ${config.globals.username} users -"
    "r /home/${config.globals.username}/.config/mpv/mpv.conf"
    "L+ /home/${config.globals.username}/.config/mpv/mpv.conf - - - - ${mpvConf}"
  ];

}

Here, I first define the contents of the configuration file, then define the package itself, and finally delete and create a symbolic link to the file in the .config directory (in this case .config/mpv).

Conclusion

I hope this post serves as an introduction to creating your own NixOS configurations. In my case, I’m still learning how to use this operating system, and everything shown here represents references I wish I had when I started.

NixOS is not a distribution for everyone, but for those who value reproducibility and full control over the system, the initial effort is worth it. The code is available on my GitHub for anyone who prefers to use it as a reference alongside this article.


  1. This file will not appear in the public repository for security reasons, since it may store sensitive data such as passwords. ↩︎

#self-hosted

Reply to this post by email ↪