Linux

How do I keep a background tool running with systemd --user?

30 September 2026 6 min read

Write a unit file in ~/.config/systemd/user/, set Restart=on-failure, and enable it with systemctl --user enable --now. systemd then starts the process when your desktop session starts, restarts it if it crashes, and sends its output to the journal, where journalctl --user can read it. User scope is the right choice for anything that needs your microphone, your display, or your keystrokes, because those belong to your login session and not to the machine.

A hotkey-triggered dictation daemon makes a good worked example. It has to be running before you need it, survive its own bugs, and touch hardware that only exists inside your session.

Why a plain background process isn't enough

The usual shortcut is to launch the tool from your window manager's autostart file, or to run nohup mytool & in a terminal. Both work until they don't. If the process crashes, nothing restarts it, and you find out when you press the hotkey and nothing happens. There's no log either, unless you remembered to redirect stderr somewhere.

That failure is worse for a dictation tool, because the hotkey gives no feedback when the daemon is dead. You press Alt+D, talk for ten seconds, and then notice nothing was typed. Autostart scripts also give you no ordering. Your tool may start before PipeWire is ready and fail once on a missing audio socket, with nobody to retry it.

A service manager already solves these problems: dependency ordering, restart policy, log capture, and one consistent way to check status. Reimplementing them in a shell script is how people end up with a pidfile bug.

The pattern: a user service with a restart policy

Here is a minimal unit for a daemon like this. The name and path are illustrative, so adjust them for your tool.

[Unit]
Description=Hotkey dictation daemon
After=graphical-session.target
PartOf=graphical-session.target
StartLimitIntervalSec=60
StartLimitBurst=5

[Service]
ExecStart=%h/.local/bin/mydictationd
Restart=on-failure
RestartSec=2

[Install]
WantedBy=graphical-session.target

Save it as ~/.config/systemd/user/mydictationd.service, then run systemctl --user daemon-reload and systemctl --user enable --now mydictationd. A few details matter here.

Restart=on-failure restarts after a non-zero exit, a signal, or a timeout, but not after a clean exit. That's usually what you want, since systemctl --user stop shouldn't trigger a restart loop. Restart=always also restarts after clean exits.

RestartSec and the start limit keep a broken build from spinning the CPU. By default, systemd gives up after five starts in ten seconds and marks the unit failed. Once you've fixed the cause, clear that state with systemctl --user reset-failed mydictationd.

graphical-session.target ties the service to your desktop session. It starts when you log in graphically and stops when you log out. Some setups don't activate that target reliably, so if the unit never starts, check systemctl --user status graphical-session.target before blaming the unit.

Why user scope, not a system service

A system service runs outside your login. It has no $XDG_RUNTIME_DIR, no session D-Bus, and no access to your PipeWire or PulseAudio socket. You can force it to work with extra permissions and environment hacks, but you'd be giving a long-running process broader access than it needs in order to imitate what your session already provides.

User scope is also the smaller privilege footprint. The service runs as you, can't touch other users' files, and needs no root to install or remove. For a tool that listens to a microphone, that matters. The service can only reach the audio devices your session already has open to you.

Voxtty, a free Linux dictation tool, runs this way. Press Alt+D, speak, and it types the words into the focused app, with faster-whisper transcribing on-device so no audio leaves the machine. Voice activity detection decides when you've stopped talking, and the daemon sits idle until the hotkey fires. Try Voxtty free if you want to see the pattern in a real service.

Debugging with journalctl

Once the service is in the journal, debugging gets much less guessy:

systemctl --user status mydictationd
journalctl --user -u mydictationd -f
journalctl --user -u mydictationd -b -p err
journalctl --user -u mydictationd --since "10 min ago"

The first shows the current state and the last few log lines. The second follows output live, so you can press the hotkey and watch what happens. The third shows errors since boot, and the fourth narrows to a recent window. A crash loop shows up as repeated start and exit lines with timestamps, which tells you far more than "it stopped working".

Where this breaks down

Environment variables are the usual trap. A user service doesn't automatically inherit WAYLAND_DISPLAY or DISPLAY in every setup. If your tool works from a terminal but fails under systemd, run systemctl --user show-environment and compare. Many desktop environments handle the import at login, and the fix on others is dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY in your session startup.

Text injection has its own requirement. On Wayland, typing into other apps generally goes through ydotool, which needs its ydotoold daemon running and permission to write to /dev/uinput. That's a udev or group-membership question, and no unit file fixes it.

Local transcription has limits too. Whisper models handle clear speech in a quiet room well, but strong accents, background noise, and unusual technical vocabulary all lower accuracy. It also won't match the speed of top-tier cloud speech-to-text, especially on a CPU with a larger model. And a wake word, where the daemon listens continuously instead of waiting for a hotkey, is still experimental. Continuous listening costs more CPU and produces more false triggers than a keypress does.

Lastly, loginctl enable-linger keeps user services running without a login session. For a desktop tool that needs your mic and display, that's the wrong behaviour, so leave it off.

One thing to do today

Pick one script you currently start from an autostart file, and write a ten-line unit for it. Enable it with systemctl --user enable --now, then kill the process with kill -9 and watch systemd bring it back. Run journalctl --user -u yourunit -f while you do it. Ten minutes on that experiment will tell you whether the pattern suits your setup.

Try Voxtty free

Local-first voice dictation for Linux. Press Alt+D, speak, and your words land in whatever app has focus โ€” nothing leaves your machine.

Try Voxtty free โ†’
โ† Back to blog