Activation Reload/Restart Deprecation
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:
- Now (26.05): stop using the activation restart/reload lists. Prefer unit options and systemd-native prep.
- Soon (multi-release): shrink
system.activationScriptsitself — 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 || trueEmpty 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-pagerRepresentative 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-pagerDo 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-ticketRepresentative 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 lineRepresentative 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-*-listfragments. Grep the desk flake and delete what you find. - Express “restart when X changes” with
restartTriggers/reloadIfChanged/restartIfChangedon 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
- On a disposable 26.05 VM flake,
rgforactivation-restart-list,activation-reload-list, andsystem.activationScripts. File a ticket for every out-of-tree hit. - Import Case 2’s
desk_ticket_restart.nix, switch, change the conf string, switch again, and confirmdesk-ticket.servicerestarted viajournalctl -u desk-ticket. - Convert one real
mkdir/chownactivation fragment on the lab host tosystemd.tmpfiles.settings(Case 4). Diffls -ldbefore/after. - Add Case 6’s oneshot, run
nixos-rebuild switchtwice, and confirm/var/lib/desk-lab/activations.loggains a line each time. - Skim the Replacing activationScripts wiki page and the 26.05 release-notes bullet; paste the three-step ladder into the desk runbook.