Starting systemd services sharing a session D-Bus on headless system

You need several things to make this work.

  1. Enable user services to run at boot time without user login (systemd linger).
  2. A systemd socket file to specify the D-Bus socket for systemd to allocate.
  3. A systemd service to launch the D-Bus session bus that launches, then sets the DBUS_SESSION_BUS_ADDRESS env var for other systemd services.
  4. Ensure your systemd my-dbus-client.service files are of Type=dbus or depend on the dbus.socket unit to make sure they allocate the dbus session bus socket and start the dbus session service if it hasn't already been started.

First, to make Systemd services for a given user start at boot-time without login, you need to enable systemd user lingering - this only needs to be done once as root when configuring to enable it for a user:

# loginctl enable-linger otheruser

Next, if you're on a Debian-based system, for the next two steps, you can simply install the package dbus-user-session package:

# apt-get install dbus-user-session

If you're using some other distribution, want to do this manually, or just want to understand how it works continue on. Otherwise skip over the creation of dbus.service and dbus.socket.

Create the file /usr/lib/systemd/user/dbus.socket (note, on some distributions the user directory may be under /lib instead of /usr/lib) with the following content:

[Unit]
Description=D-Bus User Message Bus Socket

[Socket]
ListenStream=%t/bus
ExecStartPost=-/bin/systemctl --user set-environment DBUS_SESSION_BUS_ADDRESS=unix:path=%t/bus

[Install]
WantedBy=sockets.target
Also=dbus.service

Propagation of DBUS_SESSION_BUS_ADDRESS to all services, which was your main concern, is addressed by the ExecPostStart line below - all following services will have that set.

%t gets replaced with the XDG_RUNTIME_DIR - a transient directory under /run created by systemd specific to the user session that you can stuff files. If you wish to create this socket somewhere else, there's no reason you can't. Just make sure it's somewhere transient, or it gets cleaned up on reboot/session teardown.

I did have some issues trying to make the dbus unix socket an abstract one - systemd didn't seem to like the form unix:abstract= or @ prefix for some reason.

Now create the file /usr/lib/systemd/user/dbus.service with the following content:

[Unit]
Description=D-Bus User Message Bus
Requires=dbus.socket

[Service]
ExecStart=/usr/bin/dbus-daemon --session --address=systemd: --nofork --nopidfile --systemd-activation
ExecReload=/usr/bin/dbus-send --print-reply --session --type=method_call --dest=org.freedesktop.DBus / org.freedesktop.DBus.ReloadConfig

[Install]
Also=dbus.socket

There is a little bit of magic that goes on here behind the scenes by systemd to pass in the already created unix socket to the dbus-daemon. Systemd uses the information from dbus.socket to create the socket, and it's file descriptor gets set in the environment variable LISTEN_FDS, which is passed into the dbus-daemon. Special options listed above make dbus-daemon use the file descriptor passed in instead of creating a new one. This allows dbus clients to start parallel to the dbus-daemon starting without worries of the socket not existing.

Finally, create your own systemd user services, making sure that you either set the type to Type=dbus, set BusName= to the name of one of the dbus service names that will be registered by this service, or by making sure Requires=dbus.socket is specified in the Unit section. Here is an example:

[Unit]
Description=Config Server Startup

[Service]
Type=dbus
BusName=com.example.app.configuree
ExecStart=/opt/example/app/configuration_server
Restart=on-failure

[Install]
WantedBy=default.target

You can place them in one of several places:

  • $HOME/.config/systemd/user
  • /usr/lib/systemd/user

Enable your services with systemctl --user enable <service name> and reboot, and everything should work.

For systemctl --user .. to work, you need to have a full systemd login environment for the /run/user/{uid} to exist. The lightweight environments created by su - .. --login or sudo do not set this up. You need to ssh in, log into a console or, if you run a properly set up systemd distribution, you can grab and use machinectl shell to create a full systemd environment in your current shell.


References:

  • man loginctl for linger
  • man pam_systemd for XDG_RUNTIME_DIR info
  • man systemd.service for Type=dbus, BusName=, and implicit dependency on dbus.socket
  • man sd_listen_fds for info about LISTEN_FDS environment variable
  • https://wiki.archlinux.org/index.php/Systemd/User - general information on systemd user sessions