Activation Reload/Restart Deprecation

Updated

October 31, 2026

Activation Reload/Restart Deprecation

NixOS 26.05 deprecated reloading or restarting systemd units from the NixOS activation script, with removal targeted for 26.11. The boring default is: stop writing unit names into /run/nixos/activation-restart-list or /run/nixos/activation-reload-list from system.activationScripts; declare restart/reload behavior on the unit itself (restartTriggers, reloadIfChanged, restartIfChanged), and migrate other activation work to systemd.tmpfiles.settings, ExecStartPre=, or a oneshot service ordered with sysinit-reactivation.target.

Activation scripts used to be the catch-all “run shell after every switch.” That model serializes boot, fights parallelism, and is on a multi-release path toward removal (see the nixpkgs tracking effort). The 26.05 release notes call out the reload/restart side channel first because nixpkgs itself no longer uses it. Desk configs that still poke those list files will keep working through 26.05 with deprecation pressure, then break on 26.11.

Mental model

nixos-rebuild switch / boot
        │
        ▼
 activation script fragments     ← system.activationScripts.*
   (legacy: echo unit >> /run/nixos/activation-restart-list)
        │
        ▼
 switch-to-configuration
   reads restart/reload lists    ← DEPRECATED path (gone in 26.11)
   OR applies unit metadata      ← preferred: restartTriggers / reloadIfChanged
        │
        ▼
 systemd starts / reloads / restarts units
Piece Role
system.activationScripts Shell fragments run on activate (boot + switch); being phased out over several releases
/run/nixos/activation-restart-list Legacy side channel: activation script asks STC to restart units
/run/nixos/activation-reload-list Legacy side channel: activation script asks STC to reload units
systemd.services.<name>.restartTriggers Declared restart when listed store paths / strings change
systemd.services.<name>.reloadIfChanged Prefer reload over restart when the unit file changes
systemd.services.<name>.restartIfChanged Default-ish restart policy for unit file changes
systemd.tmpfiles.settings Create dirs/files/permissions without an activation script
serviceConfig.ExecStartPre Prep work bound to the service that needs it
sysinit-reactivation.target Re-run oneshots on every activation (switch), not only cold boot
switch-to-configuration (26.05+) Reloads (not restarts) when the only unit change is ExecReload=

Two timelines, one desk:

  1. Now (26.05): stop using the activation restart/reload lists. Prefer unit options and systemd-native prep.
  2. Soon (multi-release): shrink system.activationScripts itself — same migration ladder as the wiki: tmpfiles → ExecStartPre → dedicated oneshot.

Worked examples

Case 1: Find legacy restart/reload side channels on the desk

On a NixOS 26.05 host (or a flake eval against 26.05), hunt for the deprecated pattern before you bump toward 26.11:

# Search the desk flake / config tree
rg -n 'activation-restart-list|activation-reload-list|dry-activation-restart-list|dry-activation-reload-list' .
rg -n 'system\.activationScripts' --glob '*.nix' .

Representative hit (anti-pattern — do not keep):

# legacy_restart_from_activation.nix  — remove before 26.11
{ pkgs, ... }:

{
  system.activationScripts.deskTicketReload = ''
    # Ask switch-to-configuration to restart a unit after activation.
    # DEPRECATED in 26.05; REMOVED in 26.11.
    if [ -d /run/nixos ]; then
      echo "desk-ticket.service" >> /run/nixos/activation-restart-list
    fi
  '';
}

Also inspect whether anything still echoes reload lists:

# After a dry run on a lab VM that still has the legacy fragment
sudo nixos-rebuild dry-activate --flake .#desk-workstation
# If present (legacy only):
sudo cat /run/nixos/dry-activation-restart-list 2>/dev/null || true
sudo cat /run/nixos/dry-activation-reload-list 2>/dev/null || true

Empty files / missing paths after migration are success. Treat any remaining echo into those lists as a 26.11 blocker ticket.

Case 2: Restart when a config file changes — use restartTriggers

Desk microservice whose unit should restart when its generated config store path changes. Save as desk_ticket_restart.nix:

# desk_ticket_restart.nix
{ config, pkgs, lib, ... }:

let
  ticketConf = pkgs.writeText "desk-ticket.conf" ''
    listen = 127.0.0.1:9471
    desk = family-lab
  '';
in
{
  systemd.services.desk-ticket = {
    description = "Desk ticket stub API";
    wantedBy = [ "multi-user.target" ];
    after = [ "network.target" ];

    # When the store path of ticketConf changes, STC restarts this unit.
    # No activation-script side channel required.
    restartTriggers = [ ticketConf ];

    serviceConfig = {
      Type = "simple";
      ExecStart = "${pkgs.python3}/bin/python3 -m http.server 9471 --bind 127.0.0.1";
      # Real desks pin WorkingDirectory / a real binary; stub keeps the lab cheap.
    };
  };

  # Ship the file somewhere inspectable (optional for the stub).
  environment.etc."desk-ticket/desk-ticket.conf".source = ticketConf;
}

Apply and verify the trigger is wired:

sudo nixos-rebuild switch --flake .#desk-workstation
systemctl show desk-ticket.service -p FragmentPath -p DropInPaths
# Bump the conf contents → rebuild → expect a restart of desk-ticket.service
sudo nixos-rebuild switch --flake .#desk-workstation
systemctl status desk-ticket.service --no-pager

Representative status after a conf-triggered restart:

● desk-ticket.service - Desk ticket stub API
     Loaded: loaded (/etc/systemd/system/desk-ticket.service; enabled)
     Active: active (running) since …

restartTriggers is the boring replacement for “activation script noticed a file moved, please restart unit X.”

Case 3: Prefer reload when only ExecReload= changed

NixOS 26.05 also tightened switch-to-configuration itself: when the only change to a unit is ExecReload=, STC reloads instead of restarting; removing ExecReload= alone no longer forces a restart. Declare reload-friendly units explicitly:

# desk_proxy_reload.nix
{ pkgs, ... }:

{
  systemd.services.desk-proxy = {
    description = "Desk reverse-proxy stub";
    wantedBy = [ "multi-user.target" ];
    reloadIfChanged = true;

    serviceConfig = {
      Type = "simple";
      ExecStart = "${pkgs.nginx}/bin/nginx -g 'daemon off;' -c /etc/desk-proxy/nginx.conf";
      ExecReload = "${pkgs.nginx}/bin/nginx -s reload";
      # Keep a minimal conf out of band or via environment.etc — lab sketch only.
    };
  };
}

Operator check after editing only the reload command (or a drop-in that only touches ExecReload=):

sudo nixos-rebuild dry-activate --flake .#desk-workstation
# Expect reload planning for desk-proxy, not a full stop/start, when that is the sole diff.
sudo nixos-rebuild switch --flake .#desk-workstation
journalctl -u desk-proxy.service -n 20 --no-pager

Do not paper over “I need a restart” by writing the unit name into an activation reload list. Set reloadIfChanged / restartTriggers / restartIfChanged on the service.

Case 4: Replace “mkdir + chown” activation with tmpfiles

Classic activation fragment that only prepares directories:

# legacy_cache_dir.nix  — anti-pattern
{
  system.activationScripts.deskCache = ''
    mkdir -p /var/cache/desk-ticket
    chown desk-ticket:desk-ticket /var/cache/desk-ticket
    chmod 0750 /var/cache/desk-ticket
  '';
}

Boring replacement (desk_cache_tmpfiles.nix):

# desk_cache_tmpfiles.nix
{
  users.users.desk-ticket = {
    isSystemUser = true;
    group = "desk-ticket";
  };
  users.groups.desk-ticket = { };

  systemd.tmpfiles.settings."desk-ticket" = {
    "/var/cache/desk-ticket".d = {
      mode = "0750";
      user = "desk-ticket";
      group = "desk-ticket";
    };
  };
}

Verify:

sudo nixos-rebuild switch --flake .#desk-workstation
systemd-tmpfiles --cat-config | rg desk-ticket
ls -ld /var/cache/desk-ticket

Representative listing:

drwxr-x--- 2 desk-ticket desk-ticket 4096 … /var/cache/desk-ticket

Case 5: Prep that belongs to one service → ExecStartPre=

When the script only exists so some-service can start cleanly, attach it to that service:

# desk_ticket_prestart.nix
{ pkgs, ... }:

let
  seed = pkgs.writeShellApplication {
    name = "desk-ticket-seed";
    runtimeInputs = [ pkgs.coreutils ];
    text = ''
      set -euo pipefail
      install -d -m 0750 /var/lib/desk-ticket
      if [ ! -f /var/lib/desk-ticket/seeded ]; then
        date -u +"seeded at %Y-%m-%dT%H:%M:%SZ" > /var/lib/desk-ticket/seeded
      fi
    '';
  };
in
{
  systemd.services.desk-ticket = {
    description = "Desk ticket stub with seed preStart";
    wantedBy = [ "multi-user.target" ];
    serviceConfig = {
      Type = "simple";
      ExecStartPre = [ "${seed}/bin/desk-ticket-seed" ];
      ExecStart = "${pkgs.python3}/bin/python3 -m http.server 9471 --bind 127.0.0.1";
      StateDirectory = "desk-ticket";  # prefer this when a plain state dir is enough
    };
  };
}

Prefer StateDirectory= / CacheDirectory= / RuntimeDirectory= when they already express the prep. Use ExecStartPre= for custom seed logic that still must run immediately before that unit.

Case 6: Work that must re-run on every switch → oneshot + sysinit-reactivation.target

Activation scripts run on boot and on nixos-rebuild switch. A oneshot wanted only by multi-user.target runs on boot, not necessarily again on every activation. For “run again after each switch,” pull in sysinit-reactivation.target (desk pattern from current NixOS practice):

# desk_sync_oneshot.nix
{ pkgs, ... }:

{
  systemd.services.desk-lab-sync = {
    description = "Re-sync desk lab marker on every activation";
    wantedBy = [ "sysinit-reactivation.target" ];
    after = [ "local-fs.target" ];
    path = [ pkgs.coreutils ];

    serviceConfig = {
      Type = "oneshot";
      RemainAfterExit = true;
    };

    script = ''
      set -euo pipefail
      mkdir -p /var/lib/desk-lab
      date -u +"activated %Y-%m-%dT%H:%M:%SZ" >> /var/lib/desk-lab/activations.log
    '';
  };
}

Exercise:

sudo nixos-rebuild switch --flake .#desk-workstation
systemctl status desk-lab-sync.service --no-pager
wc -l /var/lib/desk-lab/activations.log
sudo nixos-rebuild switch --flake .#desk-workstation
wc -l /var/lib/desk-lab/activations.log   # expect +1 line

Representative log growth:

1 /var/lib/desk-lab/activations.log
2 /var/lib/desk-lab/activations.log

Order relative to user/group managers when needed (after = [ "userborn.service" ]; or the equivalent on your pin) instead of inventing activationScripts.*.deps strings.

The trap

Trap A — keep echoing into /run/nixos/activation-restart-list “until 26.11.” The list is already deprecated on 26.05; waiting until the removal release is how you get a surprise outage during an otherwise calm channel bump. Migrate on 26.05 while the old path still warns instead of fails.

Trap B — replace every activation fragment with a oneshot, skipping tmpfiles and ExecStartPre=. You pad the unit graph without gaining clarity. Follow the wiki ladder: tmpfiles → ExecStartPre → dedicated service.

Trap C — assume a oneshot wantedBy = [ "multi-user.target" ] re-runs on every nixos-rebuild switch. Cold boot ≠ activation. Use sysinit-reactivation.target when you need switch-time re-entry.

Trap D — disable reloadIfChanged / ignore STC’s smarter reload behavior, then force restarts via activation lists “to be safe.” Prefer declarative reload/restart metadata on the unit. Let 26.05 STC reload when only ExecReload= changed.

# Anti-pattern — side-channel restart
{
  system.activationScripts.forceNginx = ''
    echo "nginx.service" >> /run/nixos/activation-restart-list
  '';
}
# Boring fix — declare it on the unit
{
  systemd.services.nginx = {
    reloadIfChanged = true;
    restartTriggers = [ /* conf store paths */ ];
  };
}

The boring rule

  • On NixOS 26.05, treat activation-script reload/restart lists as deprecated; plan for removal in 26.11.
  • Never add new echo … >> /run/nixos/activation-*-list fragments. Grep the desk flake and delete what you find.
  • Express “restart when X changes” with restartTriggers / reloadIfChanged / restartIfChanged on the systemd service.
  • Migrate other activation work in order: systemd.tmpfiles.settings → ExecStartPre= → oneshot service.
  • For “run again on every switch,” use a oneshot wantedBy = [ "sysinit-reactivation.target" ], not a new activation script.
  • Expect STC on 26.05 to reload (not restart) when the only unit change is ExecReload=.
  • Track the broader activation-script deprecation (nixpkgs tracking + wiki); this chapter is the reload/restart cut that lands first.

Try this

  1. On a disposable 26.05 VM flake, rg for activation-restart-list, activation-reload-list, and system.activationScripts. File a ticket for every out-of-tree hit.
  2. Import Case 2’s desk_ticket_restart.nix, switch, change the conf string, switch again, and confirm desk-ticket.service restarted via journalctl -u desk-ticket.
  3. Convert one real mkdir/chown activation fragment on the lab host to systemd.tmpfiles.settings (Case 4). Diff ls -ld before/after.
  4. Add Case 6’s oneshot, run nixos-rebuild switch twice, and confirm /var/lib/desk-lab/activations.log gains a line each time.
  5. Skim the Replacing activationScripts wiki page and the 26.05 release-notes bullet; paste the three-step ladder into the desk runbook.