Day 27 — HM user services
Day 27 — HM user services
Stage III · ~4h
Goal: Run user-level systemd services and timers through Home Manager—work that belongs to a login user, not to the system-wide daemon set.
Why this day exists
Day 17 used system systemd via NixOS. Not everything should be system-wide:
| Better as user service | Better as system service |
|---|---|
Personal backup of ~/docs |
PostgreSQL, Caddy, sshd |
| Wait-for-graphical clipboard tool | Network-facing APIs |
| User-specific sync agent | Multi-user shared daemons |
Periodic git fetch of notes |
CI runners for a team |
HM exposes a structured way to declare user units so they survive reinstalls.
Theory 1 — Systemd user vs system
| Bus | Unit location idea | Starts when |
|---|---|---|
| System | /etc/systemd/system (generated) |
Boot / system targets |
| User | ~/.config/systemd/user (generated by HM) |
User session / lingering |
Lingering
If you need user services without an interactive login (headless user timers):
sudo loginctl enable-linger alice
loginctl show-user alice | grep LingerWithout linger, user services may only run while logged in (depending on setup). For a server lab admin user, lingering is often desirable; for a laptop, maybe not.
Theory 2 — HM service options
Home Manager provides:
systemd.user.services.<name>
systemd.user.timers.<name>
services.<hm-module>for popular user apps (where modules exist)
Raw unit example:
# homes/alice/services.nix
{ pkgs, ... }:
{
systemd.user.services.lab-heartbeat = {
Unit = {
Description = "Write a heartbeat timestamp";
};
Service = {
Type = "oneshot";
ExecStart = "${pkgs.bash}/bin/bash -c 'date -Is >> %h/lab-heartbeat.log'";
};
};
systemd.user.timers.lab-heartbeat = {
Unit = {
Description = "Run lab-heartbeat every 15 minutes";
};
Timer = {
OnBootSec = "2min";
OnUnitActiveSec = "15min";
Persistent = true;
};
Install = {
WantedBy = [ "timers.target" ];
};
};
}%h is systemd specifier for the user home directory.
Import from homes/alice/default.nix.
Theory 3 — Mapping to system Day 17 knowledge
| Concept | System (NixOS) | User (HM) |
|---|---|---|
| Service attrset | systemd.services.* |
systemd.user.services.* |
| Timer | systemd.timers.* |
systemd.user.timers.* |
| journal | journalctl -u foo |
journalctl --user -u foo |
| restart | systemctl restart |
systemctl --user restart |
Permissions: user services cannot bind privileged ports (<1024) without capabilities setup; they should not replace system network daemons.
Theory 4 — Simple vs oneshot vs long-running
| Type | Use |
|---|---|
oneshot |
Task that exits; good with timers |
simple / default |
Long-running process |
exec |
systemd newer behavior; fine when you know it |
For a periodic script: oneshot service + timer.
For a personal agent: long-running service with Restart = "on-failure".
Theory 5 — Paths and packages in units
Always use store paths from pkgs, not assumed global binaries:
ExecStart = "${pkgs.restic}/bin/restic snapshots";
# not: ExecStart = "restic snapshots";Environment:
Service = {
Environment = [
"FOO=bar"
];
# EnvironmentFile must not point at secrets in the flake/store
};Sensitive env files belong to Stage IV secret deployment paths with tight permissions—not home.file with tokens.
Theory 6 — Prefer HM modules when they exist
Examples (availability depends on HM version):
services.syncthing(user)
services.gpg-agent
- Various desktop helpers
Pattern: search HM options first; fall back to raw systemd.user.services for custom scripts.
Worked example — Notes fetcher timer
{ pkgs, ... }:
{
home.packages = [ pkgs.git ];
systemd.user.services.fetch-notes = {
Unit.Description = "Fetch personal notes repo";
Service = {
Type = "oneshot";
WorkingDirectory = "%h/notes";
ExecStart = "${pkgs.git}/bin/git fetch --all --prune";
};
};
systemd.user.timers.fetch-notes = {
Unit.Description = "Timer for fetch-notes";
Timer = {
OnCalendar = "hourly";
Persistent = true;
};
Install.WantedBy = [ "timers.target" ];
};
}Requires ~/notes to be a git repo. Create it in the lab if missing (empty repo is fine).
Lab 1 — User systemd baseline
systemctl --user status
systemd-analyze --user blame 2>/dev/null | head || true
loginctl show-user "$USER" | grep -E 'Linger|State'Decide whether to enable linger for your lab user; document the choice.
Lab 2 — Heartbeat oneshot + timer
Add the heartbeat service from Theory 2 (or equivalent). Rebuild:
sudo nixos-rebuild switch --flake .#labAs user:
systemctl --user daemon-reload # usually handled by activation; run if needed
systemctl --user list-timers --all | grep -i heart || systemctl --user list-timers --all
systemctl --user start lab-heartbeat.service
systemctl --user status lab-heartbeat.service
cat ~/lab-heartbeat.logEnable timer if not already wanted:
systemctl --user enable --now lab-heartbeat.timer
systemctl --user list-timers | grep lab-heartbeatLab 3 — Journal debugging
journalctl --user -u lab-heartbeat.service -n 50 --no-pagerBreak ExecStart deliberately, rebuild, read the failure, fix.
Lab 4 — One service you actually want
Ideas:
git fetchon a notes repo
rsyncdry-run of a directory to a backup path (no secrets in unit)
- Generate a dated file in
~/lab/
Implement, enable, prove timer or manual start works twice.
Lab 5 — Boundary check
Write four lines:
- Why this is not a system service
- What user it runs as
- Whether linger is required
- What happens on logout without linger
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Timer never fires while logged out | Enable linger or accept session-bound life |
systemctl without --user |
You queried the system bus |
| Binary not found | Use ${pkgs.foo}/bin/foo |
| Service works manually, not on timer | Check WantedBy, enable timer, list-timers |
| Putting nginx in user systemd | Use NixOS system module instead |
Secrets in Environment= in flake |
Stage IV; remove from git |
| Activation OK but unit missing | Confirm HM user matches logged-in user |
Checkpoint
- At least one user service defined via HM
- At least one timer or a long-running user service
- Can use
systemctl --userandjournalctl --user
- Linger decision documented
- No secrets in unit text
- Clear system-vs-user boundary write-up
Commit
git add .
git commit -m "day27: HM user services and timers"Write three personal gotchas before continuing.
Tomorrow
Day 28 — Dev shells & direnv: project-scoped toolchains with flakes + direnv, so the system/HM profiles stay lean.