NixOS is awesome in how it allows to specify the entire system configuration declaratively. Paired with tools like colmena one can manage a whole fleet of NixOS machines in a single, coherent repository.

That comes with downsides, when the system’s configuration has to change during an emergency. Recently, I had an ISP outage that made NextDNS completely inaccessible. My whole LAN is based on the premise that my DNS server at 192.168.53.53 can resolve the queries, and it was failing to reach NextDNS. Normally, I’d just rebuild the system with something else (e.g. CoreDNS) to survive the networking issues, but unfortunately I don’t have any reliable derivation of CoreDNS cached, and I couldn’t reach cache.nixos.org to download it either. I was stuck in a loop of needing the DNS to run the nixos-rebuild switch that would unbork the DNS.

Eventually, I remembered that I have an escape hatch in my router that filters some of the homelab kubernetes’s cluster traffic through a VPN, and I sent my existing DNS resolver to tunnel via the same VPN, allowing me to finally rebuild it, switch to CoreDNS and stabilize the networking.

That got me thinking. I got lucky in that I could easily flip the traffic onto a link with no network issues, but what if I couldn’t? Are there any mechanisms that would allow me to have a backup NixOS configuration for a similar scenario? And the answer is yes: the NixOS specialisations are the answer.

Specialisations are a mechanism to have additional configurations built, based on the primary system configuration. Here’s how I use them in my DNS server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# coredns.nix
{ ... }: {
  specialisation.coredns.configuration = {
    services.coredns.enable = true;
    services.coredns.config = ''
      .:53 {
        bind 192.168.53.53
        log
        errors
        forward . tls://1.1.1.1
      }
    '';
  };
}

This defines a new specialisation named coredns. By default it will inherit the whole parent config, but that can be disabled with specialisation.coredns.inheritParentConfig = false;. I don’t want to do that, because I want to keep all the networking, access, and firewall settings from the parent config.

This creates the service conflict, of course, because the nextdns service will be present in the coredns specialisation, too. It’s easy to fix that:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# nextdns.nix
{ lib, config, ... }: {
  config = lib.mkIf (config.specialisation != { }) {
    services.nextdns = {
      enable = true;
      arguments = [
        "-profile"
        "b00b1e"
        "-cache-size"
        "10MB"
        "-report-client-info"
        "-listen"
        "192.168.53.53:53"
      ];
    };
  };
}

I only enable it if there are any specialisations in this configuration (the parent one will have coredns, coredns will have none).

Now I can easily switch into the coredns mode with:

1
/run/current-system/specialisation/coredns/bin/switch-to-configuration test

The activation phase will run and the system becomes defined from the coredns specialisation. nextdns service stops, coredns starts. Great. How do I get back though? The problem is that now that the system is defined in the concept of coredns specialisation, it has forgotten about the parent one.

One option would be to nixos-rebuild switch it, or to reboot. The currently booted system is also available as /run/booted-system, so you can switch back into the main configuration with

1
/run/booted-system/bin/switch-to-configuration test

Be careful, though, as that configuration is literally the one the machine last booted from. If you did a bunch nixos-rebuild switch runs during the system lifetime, you will be reverted to the very initial configuration it literally booted from.

Specialisations are convenient for many other things, both on the server and on the desktop NixOS. Don’t hesitate to experiment!