This is a messy post, because it touches several topics and is rather specific. I hope this is ok to ask.
I have a systemd service that I want to start when a user logs in (and ideally stops when all their sessions ended). I faced the following problems:
I cannot reliably use the systemd module’s dropin mechanism, because the service is defined as template. It might be possible to get that to work, but for the sake of argument, let’s assume I can’t use systemd.
I cannot use environment.etc or systemd.tmpfiles to create a dropin in /etc, because the location links into nix-store (/etc/systemd/system → … → /nix/store/…)
I cannot passively create files in /run. I would need an activationScript and I find that disgusting (global namespace of activationScripts.* makes that feel like russian roulette, unless I generate a UUID as script name). Not working because: environment cannot write to /run, systemd.tmpfiles cannot create files with predefined content, require activationScripts.
There are many ways to hack my way around these problems, but my goal is more to learn how to use nixos idiomatically than to solve that concrete problem.
My initial expectation was that there must be some direct way to express “start that service when the user logs in”, something like systemd…user.me.wants = that-service@me. That doesn’t work because there is no such option and user@1000 is not root (cannot depend on sys-services). And so on…
Then I thought I could just create a dropin (standard systemd stuff), that-server@me.wantedBy = user@me. That just did not work for various reasons.
The controller in that example can start the service provisioning it the certs via wants (its service is statically configured with a wants=service-provisioning-the-cert). I would like users to automatically provision theirs, but I find no clean way of doing it.
I’m super confused. If you want to make a drop-in, why can’t you? Use systemd.packages. However, if you just want to add some dependencies to the user service manager you can lean on the module systemd: systemd.services."user@".wants = [ $your.service.here ]; and this get merged in just fine.
The problem here is that user@ runs with uid 1 and cannot start system-services (require root).
I could do systemd.services.“provide-cert@myuser”.wantedBy = [user@myuser], but that fails for various reasons:
This creates a new service, instead of creating a dropin for the service (even if I use overrideStrategy=dropin - not sure what the correct name is).
When I ignored that, accepting that I now have a non-template service and adding all the required fields (which i don’t want), I still had a bunch of problems that I forgot (needed to check what it was). Apparently there are issues creating drop-ins for template-instances in the systemd-module and my setup seems to be one of the cases where it just doesn’t work.
EDIT: If you meant to create a dropin for “user@” and not “user@particular-user”, that doesn’t help, because I want the service to run if that particular user logs in, not just any user. Template drop-ins seem to work reliably, but not template-instance drop-ins. The reason for this difference is aparently that user@ exists already, while user@xyz does not and the systemd module seems to think it’s creating a new unit instead of a drop-in for a not-yet existing template instance.
Concrete: Run provide-cert@my-user (should be active) whenever my-user is logged on via gui/ssh/console. provide-cert@ runs as root (needs to deploy certs). The %i (my-user) is used to find the configuration for the cert and the deploy location.
Luxury version: Undeploy on last logout.
(and I do not want to use things like pam hooks, bashrc, … because that config is part of a site-setup, not an individual user config. Pam hooks would work, but I consider that a hack)
I don’t think that’s possible to do with pure systemd dependencies. You can’t cross the user/system boundary with dependencies. At best you could have a user one-shot that calls the system bus, which you could probably make work with polkit? I was going to suggest pam but then I saw your edit
I know (now). But there is still the inverse (declare wantedBy on the system service and find a per-user target, user@x or session-x.scope). That has the same problem with templates and template-instances.
Systemd itself supports this use case via drop-ins. So that’s why I tried to create them manually (bypassing the systemd module that has the limitation).
And that would work, but requires the use of activationScripts. This is ok, but again, to me that feels wrong, shouldn’t be necessary to use such a fragile tool for such a simple use case. If it’s like this, then so be it, but I find it hard to believe that there is no better way to get it done.
So I’m not quite convinced about the reverse dependency, but template drop-ins I think work just fine. To wit:
systemd.packages = [
(pkgs.writeTextFile {
name = "override.conf";
destination = "/etc/systemd/system/user@1000.service.d/override.conf";
text = ''
[Service]
Environment="FOO=bar"
'';
})
];
yields
❯ systemctl cat user@1000
# /etc/systemd/system/user@.service
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
[Unit]
Description=User Manager for UID %i
Documentation=man:user@.service(5)
... snip ...
# /nix/store/4c7xqkc5lrv5d47g45gfgqrgc22x2vv0-system-units/user@1000.service.d/override.conf
[Service]
Environment="FOO=bar"
# /nix/store/4c7xqkc5lrv5d47g45gfgqrgc22x2vv0-system-units/user@.service.d/overrides.conf
[Unit]
[Service]
Environment="LOCALE_ARCHIVE=/nix/store/csbxgi9rywzzix0a70ib1psxbgjc93xk-glibc-locales-2.40-66>
Environment="PATH=/nix/store/rry6qingvsrqmc7ll7jgaqpybcbdgf5v-coreutils-9.7/bin:/nix/store/39>
Environment="TZDIR=/nix/store/f7yb9lhi1z8dk4x8gy3c5xf3gvn3yi1s-tzdata-2025b/share/zoneinfo"
X-RestartIfChanged=false
That is EXACTLY the kind of answer I was hoping for. I had no idea that systemd.packages exists. I will try that tomorrow and report back. Thank’s mate!
Update: The mechanism definitely works, I can create drop-ins in /etc. Huge thank you. The WantedBy does not work with user@uid, but that’s a systemd issue, not nixos-related, or probably more likely an ignorance issue on my part.
As for crossing the boundaries… what about socket activation?
The service for each user starts a socket (that only each user can access, and the service instance can verify the caller on invocation). Nothing actually starts until called.
If you really want it running, you can open that socket from a user service at login to wake it up