A systemd user manager runs services on behalf of one account. Multiple login sessions for that account normally share the same manager, rather than each shell receiving a private service universe. User unit files can live under ~/.config/systemd/user, and systemctl --user addresses the user’s manager. This provides lifecycle management with the account’s authority, not automatic isolation from every shared system resource.

When it breaks

Closing one terminal does not necessarily stop the user’s services. Another login session may remain, and the manager’s lifetime also depends on lingering and the configured stop delay. Even after the final session ends, immediate termination is not a universal rule. Applications that must follow one graphical session need an explicit lifecycle relationship rather than assuming every user unit has that scope.

Selecting the wrong manager is another common failure. Ordinary systemctl addresses the system manager, while systemctl --user needs access to the appropriate user manager and its runtime communication endpoints. A command run through a different identity or a context without session setup may fail to connect. Setting an environment variable alone neither creates the manager nor grants authority to control another account.

A user service also does not automatically inherit the environment of whichever terminal launches a control command. Required paths, configuration, and graphical-session variables must be provided through the intended mechanism. Running as a normal account reduces authority, but that account can still access shared writable files, use network sockets, and consume resources according to existing permissions and limits.

Check it quickly

Run this Python script as an administrator in a disposable Linux system with systemd, logind, PAM integration, a user D-Bus session, and account-management tools. The recorded environment was Debian 12 with systemd 252.39. It creates one temporary account, two real PAM-backed sessions, and a user service; it does not alter an existing person’s units or login state:

from pathlib import Path
from tempfile import TemporaryDirectory
import os,pwd,subprocess,time,uuid

name='xbcatm-user-'+uuid.uuid4().hex[:8];sessions=[name+'-a.service',name+'-b.service'];unit='field-worker.service'
def run(*args):
    result=subprocess.run(args,capture_output=True,text=True,timeout=20)
    assert result.returncode==0,(args,result.stderr)
    return result.stdout.strip()
def wait_for(check,seconds=5):
    deadline=time.monotonic()+seconds
    while not check():
        assert time.monotonic()<deadline,'state transition timed out';time.sleep(0.05)
with TemporaryDirectory(prefix=name+'-',dir='/run') as directory:
    root=Path(directory);root.chmod(0o755);home=root/'home';home.mkdir()
    run('useradd','--no-create-home','--user-group','--home-dir',str(home),'--shell','/bin/sh',name)
    account=pwd.getpwnam(name);uid=account.pw_uid;gid=account.pw_gid;manager=f'user@{uid}.service'
    def userctl(*args):
        return run('setpriv','--reuid='+str(uid),'--regid='+str(gid),'--init-groups',
                   'env',f'XDG_RUNTIME_DIR=/run/user/{uid}',f'DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{uid}/bus',
                   'systemctl','--user',*args)
    def manager_value(key):return run('systemctl','show',manager,'-p',key,'--value')
    try:
        units=home/'.config/systemd/user';units.mkdir(parents=True)
        (home/'worker.py').write_text("from pathlib import Path\nimport os,time\np=Path(__file__).with_name('worker-pid');t=p.with_suffix('.tmp');t.write_text(str(os.getpid()));t.replace(p)\ntime.sleep(60)\n")
        (units/unit).write_text('[Service]\nType=exec\nExecStart=/usr/bin/python3 %h/worker.py\n[Install]\nWantedBy=default.target\n')
        for path in [home,*home.rglob('*')]:os.chown(path,uid,gid)
        for session in sessions:
            run('systemd-run','--quiet','--unit='+session,'--property=Type=simple',
                '--property=User='+name,'--property=PAMName=login','/usr/bin/sleep','60')
        wait_for(lambda:Path(f'/run/user/{uid}/bus').exists())
        wait_for(lambda:len(run('loginctl','show-user',name,'-p','Sessions','--value').split())==2)
        assert run('loginctl','show-user',name,'-p','Linger','--value')=='no'
        initial=manager_value('MainPID');assert initial!='0'
        userctl('enable','--now',unit);wait_for(lambda:(home/'worker-pid').exists())
        worker_pid=(home/'worker-pid').read_text()
        assert userctl('show',unit,'-p','MainPID','--value')==worker_pid
        run('systemctl','stop',sessions[0])
        wait_for(lambda:len(run('loginctl','show-user',name,'-p','Sessions','--value').split())==1)
        assert manager_value('MainPID')==initial and userctl('show',unit,'-p','MainPID','--value')==worker_pid
        print('two PAM sessions share one user manager; closing one leaves its service unchanged')
        run('systemctl','stop',sessions[1])
        wait_for(lambda:manager_value('ActiveState')=='inactive',seconds=20)
        assert not Path('/proc/'+worker_pid).exists()
        print('after the final session and configured stop delay, non-lingering manager and worker stop')
        print('controllers use setpriv and the actual user bus; they do not create extra PAM sessions')
    finally:
        for session in sessions:
            if run('systemctl','show',session,'-p','LoadState','--value')!='not-found':run('systemctl','stop',session)
        run('systemctl','stop',manager,f'user-runtime-dir@{uid}.service')
        run('userdel',name)

The two transient session launchers use PAMName=login and the same account. The script waits until logind reports two sessions and the user bus is available. Their launch type is simple, so command return is not treated as evidence that PAM setup has completed. The explicit session and bus observations establish that boundary before user-service control begins.

The worker is enabled under the user’s default target and started through that user’s manager. It publishes its PID atomically in a harmless file, and the script compares that PID with the user unit’s actual main-process property. The system manager’s user@UID.service supplies the separate PID of the account’s manager.

Stopping the first PAM session reduces the session count to one. Both the manager PID and worker PID remain unchanged. This directly disproves the idea that each login shell owns a separate manager whose services must disappear whenever that shell ends. The surviving session keeps the same account-level service context available.

After the second session stops, the script waits for the non-lingering user manager to become inactive under the configured policy. The worker’s process is also gone. The bounded wait accommodates the documented stop delay; the result does not imply that every installation has the same delay or that unrelated shell processes follow identical termination rules.

Control commands switch identity with setpriv and address the real user bus. They do not create additional PAM sessions that would accidentally keep the manager alive and invalidate the final-session test. Cleanup stops the experiment’s remaining launchers and runtime manager, deletes the temporary account, and removes its temporary home.

Give each service the lifetime it needs

Decide whether the workload belongs to one graphical session, all sessions of an account, or an unattended machine role. A user manager is useful for account-owned background work, while linger can deliberately extend its lifetime beyond login. That choice should be explicit because it changes when resources and network listeners remain active.

Use the account’s permissions as a starting point, then add supported limits and sandboxing for the actual workload. Some execution restrictions need privileges or kernel support that are unavailable to ordinary user managers. Verify enforcement and useful behavior instead of assuming system-unit security settings transfer unchanged.

Keep unit enablement, application environment, and observable results consistent. Inspect the intended user’s units and logs, especially when several sessions or deployment tools control the same account. A shared manager simplifies supervision only when its shared lifetime and authority are part of the service’s documented contract.

Further reading

Sources