NixOS Configuration

This is my compact version of NixOS Configuration chapter that can be found on the NixOS Manual. I've added and modified a few things.

NixOS Configuration File

The NixOS configuration file generally looks like this:

{ config, pkgs, ... }:

{ option definitions
}

The first line ({ config, pkgs, ... }:) denotes that this is actually a function that takes at least the two arguments config and pkgs. The function returns a set of option definitions ({ ... }). These definitions have the form name = value, where name is the name of an option and value is its value. For example,

{ config, pkgs, ... }:

{ services.httpd.enable = true;
  services.httpd.adminAddr = "alice@example.org";
  services.httpd.documentRoot = "/webroot";
}

Options have various types of values. The most important are:

  1. Strings

    networking.hostName = "dexter";
  2. Booleans

    networking.firewall.enable = true;
  3. Integers

    boot.kernel.sysctl."net.ipv4.tcp_keepalive_time" = 60;
  4. Sets (name/value pairs enclosed in braces)

    services.httpd = {
      enable = true;
      adminAddr = "alice@example.org";
      documentRoot = "/webroot";
    };

    You can use // operator if you need to merge two attribute sets. For example:

    let
      exampleOrgCommon = {
        hostName = "example.org";
        documentRoot = "/webroot";
        adminAddr = "alice@example.org";
        enableUserDir = true;
      };
    in {
      services.httpd.virtualHosts = [
        exampleOrgCommon
        (exampleOrgCommon // {
          enableSSL = true;
          sslServerCert = "/root/ssl-example-org.crt";
          sslServerKey = "/root/ssl-example-org.key";
        })
      ];
    }
    
  5. Lists

    boot.kernelModules = [ "fuse" "kvm-intel" "coretemp" ];
  6. Packages (Usually, the packages you need are already part of the Nix Packages collection, which is a set that can be accessed through the function argument pkgs.)

    environment.systemPackages =
    [ pkgs.thunderbird
      pkgs.emacs
    ];
    
    services.postgresql.package = pkgs.postgresql_10;

    If you need to add a custom package, take a look at Adding Custom Packages chapter.

Ref: NixOS Manual

Abstraction

Functions provide another method of abstraction. For instance, suppose that we want to generate lots of different virtual hosts, all with identical configuration except for the hostname. This can be done as follows:

{
  services.httpd.virtualHosts =
    let
      makeVirtualHost = name:
        { hostName = name;
          documentRoot = "/webroot";
          adminAddr = "alice@example.org";
        };
    in
      [ (makeVirtualHost "example.org")
        (makeVirtualHost "example.com")
        (makeVirtualHost "example.gov")
        (makeVirtualHost "example.nl")
      ];
}

Here, makeVirtualHost is a function that takes a single argument name and returns the configuration for a virtual host. That function is then called for several names to produce the list of virtual host configurations.

This works because attribute values are expressions (as opposed to attribute keys).

See Abstractions chapter
how we can improve this even further (hint: by using map function).

WordPress on NixOS

Let's take a look at a real world example of a configuration.nix that deploys 2 WP blogs.
Keep in mind that when we install NixOS the /etc/nixos/configuration.nix file will already
contain some configuration. It might look something like this:

{ pkgs, ... }: {
  imports = [
    ./hardware-configuration.nix
  ];

  boot.loader.timeout = 60;
  boot.loader.grub.device = "/dev/vda";

  i18n.consoleUseXkbConfig = true;
  services.xserver.xkbVariant = "dvp";

  networking = {
    useDHCP = false;
    nameservers = [ "1.1.1.1" "1.0.0.1" ];
    defaultGateway = "185.186.999.1";
    interfaces.ens3 = {
      ipv4.addresses = [{
        address = "185.186.999.996";
        prefixLength = 24;
      }];
    };
  };

  services.openssh = {
    enable = true;
    permitRootLogin = "yes";
  };

  users.users.root.initialPassword = "secret";
  system.stateVersion = "19.09";
}

We want to keep this configuration and just add ours on top of this.

{ pkgs, ... }:
let

  # For shits and giggles, let's package the responsive theme
  responsiveTheme = pkgs.stdenv.mkDerivation {
    name = "responsive-theme";
    # Download the theme from the wordpress site
    src = pkgs.fetchurl {
      url = "http://wordpress.org/themes/download/responsive.1.9.7.6.zip";
      sha256 = "1g1mjvjbx7a0w8g69xbahi09y2z8wfk1pzy1wrdrdnjlynyfgzq8";
    };
    # We need unzip to build this package
    buildInputs = [ pkgs.unzip ];
    # Installing simply means copying all files to the output directory
    installPhase = "mkdir -p $out; cp -R * $out/";
  };

  # WordPress plugin 'akismet' installation example
  akismetPlugin = pkgs.stdenv.mkDerivation {
    name = "akismet-plugin";
    # Download the theme from the wordpress site
    src = pkgs.fetchurl {
      url = "https://downloads.wordpress.org/plugin/akismet.3.1.zip";
      sha256 = "sha256:1wjq2125syrhxhb0zbak8rv7sy7l8m60c13rfjyjbyjwiasalgzf";
    };
    # We need unzip to build this package
    buildInputs = [ pkgs.unzip ];
    # Installing simply means copying all files to the output directory
    installPhase = "mkdir -p $out; cp -R * $out/";
  };

  makeWPConfig = { name, root, aliases }: {
    "${name}" = {
      database = {
        host = "localhost";
        name = name;
        passwordFile = pkgs.writeText "wordpress-dbpass" "secret";
        createLocally = true;
      };
      themes = [ responsiveTheme ];
      plugins = [ akismetPlugin ];
      virtualHost = {
        adminAddr = "admin@localhost";
        serverAliases = aliases;
        documentRoot = root;
      };
    };
  };
in {

  networking.firewall.enable = true;
  networking.firewall.allowedTCPPorts = [ 80 443 ];
  services.httpd.adminAddr = "admin@example.com";
  services.wordpress = map makeWPConfig [
    {
      name = "foo";
      root = "/var/www/foo_com";
      aliases = [ "foo.com" "www.foo.com" ];
    }
    {
      name = "bar";
      root = "/var/www/bar_com";
      aliases = [ "bar.com" "www.bar.com" ];
    }
  ];
}

services.wordpress doesn't allow us to have different database users with createLocally
option set to true. If we want to use different database users per WP instance we need
to configure them ourselves and set the createLocally to false.

Here are all the services.wordpress options.

Ref:

Modularity

The NixOS configuration mechanism is modular. If your configuration.nix becomes too big, you can split it into multiple files.
Modules have exactly the same syntax as configuration.nix. In fact, configuration.nix is itself a module. You can use other modules by including them from configuration.nix, e.g.:

{ config, pkgs, ... }:

{ imports = [ ./vpn.nix ./kde.nix ];
  services.httpd.enable = true;
  environment.systemPackages = [ pkgs.emacs ];
  ...
}

Here, we include two modules from the same directory, vpn.nix and kde.nix. The latter might look like this:

{ config, pkgs, ... }:

{ services.xserver.enable = true;
  services.xserver.displayManager.sddm.enable = true;
  services.xserver.desktopManager.plasma5.enable = true;
}

When multiple modules define an option, NixOS will try to merge the definitions.

With multiple modules, it may not be obvious what the final value of a configuration option is. The command nixos-option allows you to find out:

$ nixos-option services.xserver.enable
true

$ nixos-option boot.kernelModules
[ "tun" "ipv6" "loop" ... ]

Interactive exploration of the configuration is possible using nix repl, a read-eval-print loop for Nix expressions. A typical use:

$ nix repl '<nixpkgs/nixos>'

nix-repl> config.networking.hostName
"ubuntu-s-1vcpu-1gb-fra1-01"

So if we take another look at our configuration.nix where we defined WP services we can
apply some modularity to it. Let's take out the WP configuration and put it in a new file
named wp.nix. Our configuration.nix will now look like this:

{ pkgs, ... }: {
  imports = [
    ./hardware-configuration.nix
    ./wp.nix
  ];

  boot.loader.timeout = 60;
  ...
}

And our wp.nix config file can stay exactly the same (as defined above). We don't need
to manually merge thous two configuration files and the structure will be cleaner and
easier to maintain.


If you need to take a quick look at nix syntax you can find it here: Syntax Summary.

Leave a Reply

Your email address will not be published. Required fields are marked *