Stop Using Sudo for Everything: A Guide to systemd User Units and Lingering

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

Why Run Services Under a User Context?

In my early days as a sysadmin, I had a dangerous habit: I ran every background script as root. It was the path of least resistance because it bypassed every permission hurdle. But as my infrastructure grew, I realized the risk. One bug in a root-level script can compromise an entire OS. That is where systemd user units change the game.

Most Linux admins use systemctl for system-wide services, yet few touch the user-level manager. This tool lets you run services with your own account’s permissions. On a standard Ubuntu 22.04 VPS, I noticed that switching from Docker-based Python workers to systemd user units dropped idle memory usage from 150MB to just 12MB. It’s a lightweight, secure alternative for tasks that don’t need full system access.

Comparing Service Management Approaches

If you need a process to stay alive in the background, you have a few options. Here is how they compare in a production environment:

Method Persistence Security Control
Screen/Tmux Lost on reboot High (User) Manual only
Crontab @reboot Persistent High (User) No auto-restart
System-wide systemd Persistent Low (Root) Excellent
systemd User Units Persistent High (User) Excellent

For non-critical tasks like web scrapers, Discord bots, or private APIs, user units offer the best balance of security and ease of use.

The Trade-offs

User units aren’t perfect for every situation. You need to know where they shine and where they struggle before migrating your stack.

The Benefits

  • Blast Radius Limitation: If an attacker exploits your service, they are trapped within your user’s permissions. They cannot touch system binaries or other users’ data.
  • Developer Autonomy: You can deploy and manage services without asking a senior admin for sudo access.
  • Clean Environments: Paths and environment variables stay scoped to your home directory, preventing “dependency hell” at the system level.

The Drawbacks

  • Resource Throttling: User services share a single resource slice. If one service leaks memory, it might cause the kernel to kill your other user processes.
  • Privileged Ports: You cannot bind to ports below 1024 (like 80 or 443) without extra configuration.
  • Session Dependency: By default, your services die when you log out. This is a common frustration that we solve with “Lingering.”

A Practical Implementation

I use user units for any Go binary or Node.js app that doesn’t need hardware-level access. I always store my service files in ~/.config/systemd/user/. This follows the XDG standard and keeps your home directory tidy.

1. Creating the Unit File

First, create the directory where systemd looks for user-level configurations.

mkdir -p ~/.config/systemd/user/

Let’s build a service for a Python worker. We want this to restart automatically if it crashes and log its output to a specific folder.

nano ~/.config/systemd/user/my-worker.service

Paste this configuration, replacing the paths with your actual project location:

[Unit]
Description=Python Background Worker
After=network.target

[Service]
ExecStart=/usr/bin/python3 -u /home/alex/scripts/worker.py
Restart=always
RestartSec=3
StandardOutput=append:/home/alex/logs/worker.log
StandardError=append:/home/alex/logs/worker.err

[Install]
WantedBy=default.target

Note: Always use absolute paths. systemd does not know about your PATH variable or .bashrc aliases.

2. Controlling the Service

When managing these services, the --user flag is mandatory. Without it, systemd will check /etc/systemd/system/ and fail to find your file.

# Refresh the manager
systemctl --user daemon-reload

# Start and enable for boot
systemctl --user start my-worker.service
systemctl --user enable my-worker.service

# Check if it is running
systemctl --user status my-worker.service

3. Enabling Persistence with Lingering

On most modern distros, the system kills all user processes the moment your last SSH session ends. To keep your service running 24/7, you must enable lingering. This tells systemd to start your user manager at boot and keep it alive indefinitely.

# Enable lingering for your specific user
sudo loginctl enable-linger alex

Once enabled, your services will start the moment the server boots, even before you log in via SSH.

Troubleshooting Common Errors

Even simple setups can hit snags. Here are two issues I encounter frequently.

The “Failed to connect to bus” Error

If you see Failed to connect to bus: No such file or directory, your XDG_RUNTIME_DIR is likely missing. This usually happens if you switched users using su - username. To fix it, run this command or add it to your profile:

export XDG_RUNTIME_DIR=/run/user/$(id -u)

Viewing Logs Without Root

You don’t need to check /var/log/syslog. Use journalctl with the user flag to see exactly what your script is doing in real-time.

journalctl --user -u my-worker.service -f

Moving away from a “root-only” mindset makes your servers more resilient. User units and lingering provide a professional, container-free way to manage background tasks with minimal overhead.

Share: