On Ubuntu systems (22.04+), every user login — including console-only SSH sessions — starts
a full systemd --user session. This triggers socket-activated and D-Bus-activated services
designed for graphical desktops, resulting in unnecessary processes consuming memory and
resources on headless or SSH-only connections.
A typical SSH login can spawn 8–15+ unnecessary processes across audio, file indexing, virtual filesystems, accessibility, desktop portals, and more — none of which serve any purpose over SSH.
We apply systemd drop-in overrides that add ConditionEnvironment=DISPLAY to the relevant
unit files. This condition ensures the services only start when a graphical display is
available (i.e., $DISPLAY is set in the environment), which is the case for desktop and
remote desktop sessions but not for plain SSH logins.
The script auto-discovers which units are actually installed on the target system and only creates overrides for those — missing units are silently skipped. This makes it safe to run across different Ubuntu versions and desktop configurations.
Masking (systemctl --user mask <unit>) would disable the services entirely — including in
graphical sessions. This breaks desktop audio, snap theming, and XDG portals for users who
also log in via a desktop environment.
ConditionEnvironment=DISPLAY is the right approach because:
- Graphical sessions (GNOME, KDE, etc.) set
$DISPLAY— services start normally. - SSH with X11 forwarding (
ssh -X) also sets$DISPLAY— services start, which is correct since forwarded X11 apps may need audio and portal access. - Plain SSH sessions do not set
$DISPLAY— services are cleanly skipped. - No state to track — unlike masks, there's no per-user state to manage or reconcile.
Services are organized into three tiers based on risk profile:
These services have no business running in a headless SSH session. No CLI tools depend on them.
| Unit | Category | Description |
|---|---|---|
pipewire.socket |
Audio (PipeWire) | Multimedia server socket activation |
pipewire-pulse.socket |
Audio (PipeWire) | PulseAudio compatibility layer |
wireplumber.service |
Audio (PipeWire) | PipeWire session/policy manager |
pulseaudio.socket |
Audio (PulseAudio) | Legacy audio server (pre-PipeWire systems) |
pulseaudio.service |
Audio (PulseAudio) | Legacy audio server service |
snapd.session-agent.socket |
Snap | Snap desktop integration (Ubuntu-specific) |
xdg-document-portal.service |
XDG Portals | Sandboxed document access |
xdg-permission-store.service |
XDG Portals | Sandboxed app permissions |
xdg-desktop-portal.service |
XDG Portals | Main portal dispatcher |
xdg-desktop-portal-gtk.service |
XDG Portals | GTK portal backend |
xdg-desktop-portal-gnome.service |
XDG Portals | GNOME portal backend |
at-spi-dbus-bus.service |
Accessibility | AT-SPI D-Bus bus for assistive tech |
tracker-miner-fs-3.service |
Indexer | GNOME Tracker filesystem indexer (CPU/IO heavy) |
tracker-xdg-portal-3.service |
Indexer | Tracker XDG portal frontend |
speech-dispatcher.service |
Speech | Text-to-speech routing daemon |
speech-dispatcherd.service |
Speech | Text-to-speech (alternate name) |
Safe on servers and SSH-only boxes. On shared workstations where users also log in
graphically, these are still safe (the $DISPLAY gate preserves graphical sessions), but
the surface area is larger.
| Unit | Category | Description |
|---|---|---|
gvfs-daemon.service |
GVFS | Core virtual filesystem daemon + FUSE |
gvfs-metadata.service |
GVFS | File metadata tracking for Nautilus |
gvfs-udisks2-volume-monitor.service |
GVFS | Local disk volume monitor |
gvfs-mtp-volume-monitor.service |
GVFS | MTP device monitor (Android phones) |
gvfs-goa-volume-monitor.service |
GVFS | GNOME Online Accounts volume monitor |
gvfs-afc-volume-monitor.service |
GVFS | Apple AFC protocol volume monitor |
evolution-addressbook-factory.service |
Evolution | Contacts backend for GNOME |
evolution-calendar-factory.service |
Evolution | Calendar backend for GNOME |
evolution-source-registry.service |
Evolution | Data source registry |
goa-daemon.service |
GNOME Online | Cloud account integration daemon |
These units require careful consideration before disabling. gnome-keyring-daemon
provides the org.freedesktop.secrets D-Bus API and optionally wraps ssh-agent. If CLI
tools on the system use libsecret for credential storage (e.g., git credential-libsecret,
NetworkManager, GNOME Passwords), gating gnome-keyring on $DISPLAY will break them in
SSH sessions.
Only enable this tier if:
- You have a separate
ssh-agentsetup (e.g., OpenSSH's native agent) - No CLI tools depend on the
org.freedesktop.secretsD-Bus API - You don't use
gnome-keyringfor SSH key passphrase caching
| Unit | Category | Description |
|---|---|---|
gnome-keyring-daemon.socket |
Keyring | GNOME Keyring socket activation |
gnome-keyring-daemon.service |
Keyring | GNOME Keyring daemon |
gcr-ssh-agent.socket |
Keyring | GCR SSH agent (split from keyring in gcr-4) |
gcr-ssh-agent.service |
Keyring | GCR SSH agent service |
Note on snap-related units:
snapd.session-agent.socketis Ubuntu/snap-specific and will not exist on Fedora, Arch, Debian, or other non-snap distributions. The script silently skips it when not found.
Overrides are placed in /etc/systemd/user/<unit>.d/graphical-only.conf as drop-in files.
This approach:
- Auto-discovers installed units — only creates overrides for services that exist on the target system. Missing units are silently skipped.
- Does not modify vendor unit files in
/usr/lib/systemd/user/, preserving clean upgrade paths. - Survives package updates — drop-ins are not overwritten by package managers.
- Is easily reversible —
revertcleans up all override files and empty directories.
- SSH sessions (
$DISPLAYnot set): Services are skipped. Only the essential session infrastructure runs (systemd --user,sd-pam,dbus-daemon,sshd, shell). - Graphical sessions (
$DISPLAYis set): All services start normally. - SSH with X11 forwarding (
ssh -X):$DISPLAYis set, so services will start.
Each override file contains:
[Unit]
ConditionEnvironment=DISPLAYScan the system to see which known units exist and their current override status:
./lean-ssh.sh discover# Default tier — safe for any system
sudo ./lean-ssh.sh apply
# Default + full tier
sudo ./lean-ssh.sh apply --full
# All tiers including keyring (read caveats first!)
sudo ./lean-ssh.sh apply --full --keyring
# Preview without making changes
sudo ./lean-ssh.sh apply --full --dry-run# Status for default tier (no root needed)
./lean-ssh.sh status
# Status for all tiers
./lean-ssh.sh status --full --keyring# Remove ALL overrides (regardless of tier)
sudo ./lean-ssh.sh revert
# Remove only keyring tier overrides
sudo ./lean-ssh.sh revert --keyringsudo ./lean-ssh.sh apply --full --quiet| Code | Meaning |
|---|---|
0 |
All units processed successfully |
1 |
No changes made (nothing to apply/revert) |
2 |
Fatal error (bad arguments, missing root, unsupported systemd) |
After applying, log out and back in via SSH, then check running processes:
ps -u $(whoami) -fYou should see only:
systemd --user
(sd-pam)
dbus-daemon --session # May show as dbus-broker on Ubuntu 24.04+
sshd: user@pts/X
-bash (or your shell)
Note: Ubuntu 24.04 and later may use
dbus-brokerinstead ofdbus-daemonfor the session bus. Both are normal and expected.
- Ubuntu 22.04 LTS (Jammy) and later
- Ubuntu 24.04 LTS (Noble) and later
- Other systemd-based distributions with PipeWire (Fedora, Arch, Debian 12+) —
verify unit file names match before applying, or use
discoverto check - Requires systemd 249+ (for
ConditionEnvironment=support) - Snap-related units are Ubuntu-specific and silently skipped on other distributions
- PulseAudio units are included for pre-PipeWire systems; both stacks are handled
To fully revert all changes:
sudo ./lean-ssh.sh revertWithout tier flags, revert removes all overrides this script could have created,
regardless of which tier was used to apply them.
Or manually:
# For each unit with an override:
sudo rm -f /etc/systemd/user/<unit>.d/graphical-only.conf
sudo rmdir /etc/systemd/user/<unit>.d 2>/dev/nullChanges take effect on next user login. To apply immediately in an active session:
systemctl --user daemon-reload