4 min read
Declaratively Managing Directories in NixOS

NixOS provides a powerful and declarative way to manage your system configuration. However, sometimes you need to perform actions that aren’t directly supported by a NixOS module. One common task is creating directories with specific ownership and permissions, especially for services that run under a dedicated user.

In this post, we’ll explore a custom Nix utility I wrote to solve this problem: create-directories.nix.

The Problem

When setting up services like Jellyfin, Sonarr, or Portainer, you often need to create configuration directories for them. These directories need to have the correct ownership and permissions so that the services can read and write to them. While you could do this manually with mkdir, chown, and chmod, that approach isn’t declarative and doesn’t fit well with the NixOS philosophy.

The Solution: create-directories.nix

To address this, I created a small Nix utility that generates a system activation script. This script checks for the existence of a list of directories and, if they don’t exist, creates them with the specified user, group, and permissions.

Here’s the code for lib/create-directories.nix:

{ lib }:

# takes in a list of directories and creates them with the proper ownership if
# they cont exist already

{
  createDirectories = { directories, user, group, mode ? "0775" }:
    lib.mkOrder 50 ''
      for dir in ${lib.concatStringsSep " " directories}; do
        if [ ! -d "$dir" ]; then
          mkdir -p "$dir"
          chown ${user}:${group} "$dir"
          chmod ${mode} "$dir"
        fi
      done
    '';
}

The createDirectories function takes a set of arguments: directories (a list of paths), user, group, and an optional mode (which defaults to “0775”). It then uses lib.mkOrder to create a system activation script that will be executed during nixos-rebuild switch.

The script iterates through the provided list of directories. For each directory, it checks if it already exists. If it doesn’t, the script creates the directory, sets the correct ownership, and applies the specified permissions.

How to Use It

Using this utility is straightforward. In your NixOS configuration, you can import the create-directories.nix file and then use it to define the directories you need.

Here’s an example of how it might be used in a default.nix for a homelab setup:

{ inputs, lib, config, pkgs, vars, ... }:

# Import the createDirectories function
let
  createDirs = import ./create-directories.nix { inherit lib; };

  directories = [
    "${vars.serviceConfigRoot}/portainer"
    "${vars.serviceConfigRoot}/jellyfin"
    "${vars.serviceConfigRoot}/jellyfin/cache"
    "${vars.serviceConfigRoot}/jellyfin/config"
    "${vars.serviceConfigRoot}/jellyseerr"
    "${vars.serviceConfigRoot}/sonarr"
    "${vars.serviceConfigRoot}/radarr"
    "${vars.serviceConfigRoot}/prowlarr"
    "${vars.serviceConfigRoot}/recyclarr"
    "${vars.serviceConfigRoot}/booksonic"
    "${vars.homelabNasMount}/Media/Downloads"
    "${vars.homelabNasMount}/Media/TV"
    "${vars.homelabNasMount}/Media/Movies"
    "${vars.homelabNasMount}/Media/Music"
    "${vars.homelabNasMount}/Media/Audiobooks"
    "${vars.homelabNasMount}/Media/Books"
  ];
in
{
  # Use the function to create directories with appropriate ownership and permissions
  system.activationScripts.createDirectories = createDirs.createDirectories {
    directories = directories;
    user = "share";
    group = "share";
    mode = "0775"; # Optional: you can override the mode if needed
  };

  # Existing services and configurations...
}

In this example, we define a list of directories that are needed for various media services. We then use the createDirectories function to generate the activation script, specifying that the directories should be owned by the share user and group.

Conclusion

This simple Nix utility demonstrates the power and flexibility of NixOS. By creating our own functions and activation scripts, we can extend the system to meet our specific needs while still maintaining a declarative and reproducible configuration. This approach is much cleaner and more maintainable than manually managing directories and permissions.