'Failed to connect to bus' — and four other things that break when a service moves to a user unit


A service that runs as a dedicated account, only ever touches that account’s home directory, and needs no privileged port does not need a root-owned unit in /etc/systemd/system. Moving it to a user unit means the owning account can start and stop it with no privilege at all, and the whole workload lives in one place.

The unit file itself is almost identical. Everything else about the move is where the time goes.

The loud one

$ sudo -u appsvc systemctl --user restart appsvc
Failed to connect to bus: No medium found

This reads like the service is broken, or systemd is broken, or the unit is missing. It is none of those. systemctl --user finds its manager through XDG_RUNTIME_DIR, and sudo -u gives you a non-login shell, which does not inherit it.

sudo -u appsvc XDG_RUNTIME_DIR=/run/user/$(id -u appsvc) systemctl --user status appsvc

Nothing was wrong. You were talking to nothing.

Because this bites on every single ad-hoc command, it is worth ten minutes to write a wrapper that sets the variable and forwards the rest — appctl restart, appctl logs. You will type it dozens of times.

The one that makes you think nothing is logging

Root cannot see a user unit’s journal:

# journalctl -u appsvc
-- No entries --

The unit is running fine. The journal moved with it. It is journalctl --user -u appsvc, as that account. If you have spent any time reaching for sudo journalctl by reflex, expect to be told your working service has never logged anything.

The one that only fails at 3am

A user manager normally starts when the user logs in and stops when they log out. A service account never logs in, so without help the unit simply never starts:

sudo loginctl enable-linger appsvc

This is the step that makes the whole thing work at boot, and it leaves no trace when it is missing. There is no failed unit, no error, no journal entry — the service is just not running. If a user-unit service is mysteriously dead after a reboot, check this before anything else:

$ loginctl show-user appsvc | grep Linger
Linger=yes

The quiet one

This is the trap worth the article.

Lingering user managers start early — earlier than the multi-user.target system unit you just replaced. If your service binds a specific address that is not up yet at that point, the bind fails.

Whether that kills the service depends on where the bind happens. In our case the app’s HTTP interface runs in a worker thread, so:

  • the listener thread died
  • the main process carried on working perfectly
  • the unit stayed active (running)
  • Restart=on-failure never fired, because nothing exited

The result is a service that is genuinely half-dead and reports itself completely healthy, indefinitely. The dashboard was simply gone until somebody happened to look.

The fix is to make the unit wait for the thing it depends on, without ever being able to block boot:

ExecStartPre=/bin/bash -c 'for i in $(seq 1 60); do ip -4 addr show | grep -q 10.20.30.40 && exit 0; sleep 2; done; exit 0'

Up to two minutes, then start regardless. Note the final exit 0 — the guard must not be able to fail the unit, or a slow network turns a degraded service into no service.

Ordering directives will not save you here, incidentally. network-online.target does not exist in the user manager, so the usual After= incantation is not available to you in this scope.

The unglamorous one

If you are migrating rather than starting fresh, delete the system unit and systemctl daemon-reload. Leaving both in place gives you two managers racing to run the same process against the same files.

And check ownership afterwards. install -d creates intermediate directories as root, so it is easy to end up with a root-owned ~/.config/systemd inside an otherwise correctly owned home. chown -R and move on.

The takeaway

Two things I would want to remember.

A user unit changes when your service starts, not just who runs it. That is the part that does not appear in any tutorial, because it only shows up on a machine whose network comes up slowly. Anything that binds a specific address, mounts a network filesystem, or talks to a local daemon at startup is now racing something it did not used to race.

active (running) is a statement about the main process and nothing else. If your service does its real work in threads, systemd’s idea of health stops at PID 1 of the cgroup. A thread can die silently and take a whole feature with it while the unit stays green and Restart= sits there with nothing to react to. When a service is “up” but a piece of it is missing, that gap is the first place to look.


Hostnames, paths, identifiers and product-specific strings in this post are illustrative. The failure modes, commands and fixes are real.