Sam's blog
March 15, 2022

NixOS - Automatically restarting a systemd unit when a local config file changes

Posted on March 15, 2022  •  3 minutes  • 493 words

The Setup

I have a service that gets its config file from my NixOS config directory, more specifically:

{ config, lib, pkgs, ... }:
with lib;
{
  environment.etc."watcher-bot/config.py".source = ../constants/watcher-config.py;

  systemd.services.watcher-bot = {
    # ... other options here
    environment = {
      PYTHONPATH = "/etc/watcher-bot";
    };
  };
}

This makes the contents of ../constants/watcher-config.py (relative to the current .nix file) available at /etc/watcher-bot/config.py.
The PYTHONPATH environment variable makes it possible to do import config from Python.

Problems

This works great, but there’s an issue: how do I automatically restart the service when the file changes? Using systemd.services.<name>.restartTriggers :

  systemd.services.watcher-bot = {
    # ... other options here
    environment = {
      PYTHONPATH = "/etc/watcher-bot";
    };
    restartTriggers = [
      config.environment.etc."watcher-bot/config.py".source
    ];
  };

But this doesn’t cause changes of the file to restart the unit! What’s going on here?

The documentation for restartTriggers says:

An arbitrary list of items such as derivations. If any item in the list changes between reconfigurations, the service will be restarted.

The way this works is by writing the contents of the list to the X-Restart-Triggers option in the unit, which changes the unit file if the list contents do (systemd does not do anything with this option).

The important part is “If any item in the list changes” - I’m just pointing it to the path of the config in the Nix config directory, and editing the file doesn’t change the path.

If you’re using pkgs.writeText or similar to write a string to the Nix store, then the path does change when the file does as it’s written to a path like /nix/store/<hash>-my-config as changing the contents changes the hash, which changes the path, which causes the unit to change, which causes Nix to restart the service.

Solution

Well, is there a way to get a file into the store without having to provide its contents as a string?
Yes, pkgs.copyPathToStore does exactly this:

{
  environment.etc."watcher-bot/config.py".source = (pkgs.copyPathToStore ../constants/watcher-config.py);
}

But we can shorten it to:

{
  environment.etc."watcher-bot/config.py".source = "${../constants/watcher-config.py}";
}

…since Nix copies paths to the store when turning paths into strings.

Now the path evaluates to something like /nix/store/<hash>-watcher-config.py, so changes to the file will cause the service to be restarted.

The usage of /etc for this is unnecessary at this point, with some restructuring I can add a directory containing the config.py to the store, and give that path in PYTHONPATH to the unit:

  systemd.services.watcher-bot = {
    # ... other options here
    environment = {
      PYTHONPATH = "${../constants/watcher}";
    };
  };

Alternate solution

You could also have Nix put the hash of the file in the restartTriggers, this is useful if:

For example, putting the config in the service’s home directory:

  systemd.services.watcher-bot = {
    # ... other options here
    environment = {
      PYTHONPATH = "/home/watcher/config/";
    };
    restartTriggers = [
      (builtins.hashFile "sha256" /home/watcher/config/config.py)
    ];
  };
Links