SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor

systemctl --user: run service for VPS, no sudo wahala

Make your Telegram bot or n8n start for boot and survive logout without root unit: ~/.config/systemd/user, loginctl enable-linger, and the bus error wey dey confuse people.

Wetin you go get when you finish

systemctl --user na the way to make service wey you install under your own account for VPS start when the server boot, and continue to run after you logout, without you writing any root unit inside /etc/systemd/system/. You go put one small file inside ~/.config/systemd/user/, run systemctl --user enable --now, then run loginctl enable-linger one time. After that, your Telegram bot, your n8n, your Ollama or your coding agent go behave like proper system service, but e go run as you, with your own permission only.

Every command here na for Ubuntu 24.04, wey dey ship systemd 255 (as of September 2026, na still that version apt go give you, because LTS no dey change major version). The same commands go work for Debian 12 and any distro wey get systemd plus logind. Where e no go work na container wey no boot systemd properly, like Docker or some LXC template, because systemctl --user need a running user manager, and na logind dey start am.

Why your bot dey die when you logout

Make we understand the wahala first, then the fix go make sense. When you SSH enter your VPS as bot (or whatever your user be), one PAM module wey dem call pam_systemd go tell logind say new session don open. Logind go do two things. First, e go create session scope, something like session-3.scope, where your shell and anything wey you start from that shell go live. Second, if na your first session, e go start user@1000.service (1000 na your UID, check am with id -u). That user@1000.service na your own private systemd, the user manager, and na im systemctl --user dey talk to.

When you logout from your last session, logind go wait small, 10 seconds by default (UserStopDelaySec= inside /etc/systemd/logind.conf), then e go stop user@1000.service. Every service wey that user manager dey run go stop join. Na why your bot dey die: the systemd wey dey carry am comot, so e carry the bot go.

So why nohup and tmux dey survive the same logout? Because Ubuntu build logind with KillUserProcesses=no. That setting mean say when session scope close, logind no go kill the process wey still dey inside am. So your nohup python bot.py & go remain, and your tmux server go remain. But nobody dey manage dem. Reboot go kill dem, and nothing go bring dem back. If na that trick you dey use now, the guide on keeping a command running after SSH disconnect explain wetin really dey happen there. E good for one job wey go finish tonight, but a bot wey suppose stay up for months need something wey go come back after reboot by itself.

loginctl enable-linger na the switch wey change all of this, and we go reach am. But first make we write the unit.

Where user unit dey live

System unit dey live inside /etc/systemd/system/, and you need root to write there. User unit get im own three places, and the user manager dey read dem for this order:

  • ~/.config/systemd/user/ na your own. No sudo. Na here every unit wey you write go go.
  • /etc/systemd/user/ na for admin wey wan give every user for the box the same unit.
  • /usr/lib/systemd/user/ na where packages dey drop dem own, like dbus.service, or pipewire.service on desktop.

Create your folder if e never dey:

mkdir -p ~/.config/systemd/user

One thing to hold: inside unit file, %h mean your home directory and %t mean your runtime directory (/run/user/1000). Use %h instead of typing /home/bot everywhere, so the same unit go still work if you copy am go another box wey get different username.

Write the unit file

Make we use Python Telegram bot as example. The code dey inside ~/bot, the venv dey ~/bot/.venv, and the token dey inside ~/bot/.env as one line, BOT_TOKEN=..., no export in front. Write ~/.config/systemd/user/bot.service:

[Unit]
Description=Telegram bot

[Service]
Type=simple
WorkingDirectory=%h/bot
EnvironmentFile=%h/bot/.env
ExecStart=%h/bot/.venv/bin/python %h/bot/bot.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

Small gist on the lines wey fit bite you.

ExecStart= must carry the full path to the Python inside the venv. If you write python bot.py, the user manager go search im own PATH, and that PATH no know your venv, so e go run the system Python wey no get your packages, and the bot go crash with import error. Full path solve am one time.

EnvironmentFile= dey read KEY=value lines and put dem inside the environment of the process. E better pass putting the token direct inside the unit file, because systemctl --user cat bot.service go show anything wey dey inside the unit to anybody wey fit run am. Make sure the .env file na chmod 600. If the file no dey at all, the service go refuse to start, which na wetin you want for a bot wey no fit work without token.

Restart=on-failure with RestartSec=5 mean say if the bot crash, systemd go wait 5 seconds and start am again. This one matter pass for user service, because the user manager no get network-online.target the way the system manager get am. When your VPS boot and linger dey on, your bot fit start before network ready, and the first call to Telegram API go fail. With Restart=on-failure, e go just try again after 5 seconds, and by that time network don dey. The restart policy guide cover the difference between on-failure and always, and the start limit wey go stop systemd from restarting am forever.

Type=simple na the default, and e correct for bot wey just start and stay for foreground. If your program dey fork go background (some older daemons dey do am), you need another type, or systemd go think say the service don die the moment the parent exit. The service type guide explain simple, exec, forking and notify, and when each one fit lie to you about whether the service don really start.

Wetin about n8n and anything from npm?

Same idea, but PATH go bite you twice. If you install n8n with nvm, the binary dey somewhere like ~/.nvm/versions/node/v22.12.0/bin/n8n, and the version number na your own. Run which n8n inside your normal shell to see the real path. The second wahala be say the n8n binary start with #!/usr/bin/env node, so even with full path to n8n, env still need to find node for PATH. The user manager PATH no get your nvm folder, so env no go see node, and the service go die immediately with exit code 127. Set PATH inside the unit:

[Service]
Environment=PATH=%h/.nvm/versions/node/v22.12.0/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=%h/.nvm/versions/node/v22.12.0/bin/n8n start

Ollama wey you download by hand, a Go binary, a Rust binary, any single static file, no get this problem, because dem no need anything else for PATH. Full path for ExecStart= na enough.

WantedBy=default.target, no be multi-user.target

If you copy unit file from any system service tutorial, the [Install] section go say WantedBy=multi-user.target. For user unit, change am to default.target. The reason simple: multi-user.target no exist inside the user manager. The user manager get im own small set of targets, default.target, basic.target, timers.target, sockets.target, paths.target and some desktop ones. default.target na the one wey the manager dey start when e boot, so na the correct place to hang your service.

Wetin go happen if you leave multi-user.target? systemctl --user enable go still create symlink inside ~/.config/systemd/user/multi-user.target.wants/, and e fit no complain at all. But nothing inside the user manager dey ever start multi-user.target, so nobody go ever pull your service. enable --now go start am one time today, and after reboot e go just no start, with no error anywhere. If your service dey start by hand but no dey start after reboot, check this line first.

Enable am with systemctl --user and check say e dey run

systemd-analyze --user verify ~/.config/systemd/user/bot.service
systemctl --user daemon-reload
systemctl --user enable --now bot.service
systemctl --user status bot.service

systemd-analyze --user verify go read the file and report typo inside directive name, or ExecStart= path wey no exist, before you even try to start am. E dey quiet when everything correct. Na the only one among these commands wey no need running user manager, so e go work even inside container or inside su shell.

daemon-reload tell the user manager to read the folder again. If you edit the unit later and forget this step, systemctl --user status go still dey run the old version, and e go print warning under the status say the unit file don change on disk.

enable --now do two things: e create the symlink under default.target.wants/ so the service go start next time the manager start, and e start am now. status suppose show active (running) in green, with the PID and the last few log lines. If e show failed, read the exit code wey dey there. The exit code guide explain wetin 203, 127 and the rest mean, and 203 for user unit almost always mean the path inside ExecStart= no correct.

For logs, journalctl --user -u bot.service, and add -f if you wan follow am live. No sudo, because the journal dey split per user and you get right to read your own. Anything your bot print to stdout dey there.

loginctl enable-linger: make e survive logout and start for boot

Everything wey we do so far go still die the moment you logout, for the reason wey we explain up. Linger na the fix:

sudo loginctl enable-linger "$USER"
loginctl show-user "$USER" -p Linger
ls /var/lib/systemd/linger/

Yes, this one line need sudo, one time only. Logind dey ask polkit whether you fit turn linger on for yourself, and polkit dey allow am freely only for "active" session, wey mean session on a local seat, like keyboard and screen wey plug into the machine. SSH session no dey on any seat, so from SSH polkit go demand admin authentication, and headless VPS no get any agent to ask you the password, so e go refuse. If polkit sef no dey the box, logind go allow only root. sudo skip the whole question. If your host give you the account without sudo, ask whoever get root to run that one line for you; after that you no need root again for anything else here.

Wetin the command really do: e create empty file /var/lib/systemd/linger/bot, and e start user@1000.service immediately if e no dey run. From now on, logind go start that user manager at boot, before anybody login, and e no go stop am when your last session close. The second command suppose print Linger=yes, and the third one suppose list your username.

Test am the proper way. Logout completely, close all your SSH sessions. Wait pass 10 seconds. Login again and run systemctl --user status bot.service. E suppose still dey active (running) with the same PID as before. Then sudo reboot, wait, login, run the same status command again. If both pass, you don finish.

If you wan see the user manager itself from outside, systemctl status user@1000.service (no --user here, and replace 1000 with your UID) go show am, with every one of your user services under am like tree.

User timer: cron without sudo

The same user manager fit run scheduled job, and you no need to touch /etc/cron.d or root crontab. Two files. First ~/.config/systemd/user/backup.service:

[Unit]
Description=Back up bot data

[Service]
Type=oneshot
ExecStart=%h/bin/backup.sh

Then ~/.config/systemd/user/backup.timer:

[Unit]
Description=Run backup every night

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target
systemctl --user daemon-reload
systemctl --user enable --now backup.timer
systemctl --user list-timers

Note say na the timer you dey enable. The service no get [Install] section at all, because na the timer go trigger am. list-timers go show you when e last run and when e go run next. Persistent=true mean say if the VPS dey off by 2am, the job go run immediately the user manager come up again, instead of skipping till tomorrow night. Without linger, that "come up again" mean your next login, so linger matter for timer the same way e matter for service. The OnCalendar= syntax and the logic na the same one wey the system-level systemd timer guide cover; only the --user flag and timers.target different.

"Failed to connect to bus": why e dey happen, and how to fix am

Sooner or later you go try run systemctl --user from a place wey no be your own normal SSH login, and e go fail with error about bus. The family of error dey talk say e no fit connect to the bus, or say the manager no dey running. The exact wording dey change between systemd versions, so I no go paste one here. The cause na wetin matter, and e get two parts.

Part one: systemctl --user dey look for the socket at $XDG_RUNTIME_DIR/bus, wey normally be /run/user/1000/bus. pam_systemd na the one wey set XDG_RUNTIME_DIR when you login through SSH. But sudo -u bot -i and su - bot no pass through logind as new session, and sudo even wipe the environment, so inside that shell the variable dey empty. Run echo $XDG_RUNTIME_DIR inside the shell and you go see nothing. Without the variable, systemctl no know where the socket dey, so e no get any bus to talk to.

Part two: if linger no dey on and the user no get any open session, user@1000.service no dey run at all, so even the directory /run/user/1000 no exist. You fit set the variable by hand and still meet nothing there.

Ways out, from the best one to the last resort.

Talk to the user manager from root with -M. Since you get sudo anyway, tell systemctl which user manager you want:

sudo systemctl --user -M bot@ status bot.service
sudo systemctl --user -M bot@ restart bot.service
sudo journalctl --user-unit=bot.service

-M bot@ na short form of --machine=bot@.host, and e mean "the user manager of user bot on this same host". Under the hood, systemd go open a proper PAM session as bot to reach am, so e go even start the user manager if e no dey run. Na why e need root: e dey create transient unit for the system manager to do that. For logs, journalctl no need the -M trick; root fit read every journal, and --user-unit= filter by user unit name across every user on the box.

Set the variables by hand inside the su shell. This one only work when the user manager already dey run, which mean linger dey on or the user get another session open somewhere:

export XDG_RUNTIME_DIR=/run/user/$(id -u)
export DBUS_SESSION_BUS_ADDRESS=unix:path=$XDG_RUNTIME_DIR/bus
systemctl --user status bot.service

Check ls -ld /run/user/$(id -u) first. If the directory no dey, the manager no dey running, and this fix no go help you.

Login as the user directly. ssh bot@your-vps go pass through pam_systemd, create proper session, and start the user manager if e no dey. Everything just work. For daily use, this one na the cleanest, and e match the idea for giving each service im own user with the least permission: one user for the bot, one for n8n, and you login as whichever one you wan manage.

One more case: fresh SSH login, and echo $XDG_RUNTIME_DIR still dey empty. That mean pam_systemd no dey inside your PAM stack. On Ubuntu 24.04 e dey come from the libpam-systemd package, wey the server image install by default, but some slim cloud image and some custom template drop am. dpkg -l libpam-systemd go tell you, and sudo apt install libpam-systemd plus a fresh login go fix am.

When system unit with User= better pass

User unit na the correct answer for "I install something under my own account and I want am to just run". But e get limit, and when you meet dem, write system unit inside /etc/systemd/system/ with User=bot under [Service] instead. The process go still run as the unprivileged user, so you no lose the security. You only move the management to root.

  • E need to bind port below 1024. Unprivileged user no fit listen on 80 or 443, and user unit no fit give am that power. System unit fit: User=bot plus AmbientCapabilities=CAP_NET_BIND_SERVICE. The other way na to keep the user service on 8080 and put nginx in front.
  • E must start before some other system service, or in fixed order at boot. Linger dey start the user manager early, but you no fit write Before=nginx.service inside a user unit, because the user manager no see system units at all. Ordering between system units only work inside system units.
  • Several admins dey manage am. User unit dey visible to that user, and to root with -M. If three people dey rotate on-call, all of dem expect sudo systemctl status thing.service to just work. System unit with User= give dem that.
  • You want the full sandbox and resource limit. MemoryMax=, ProtectSystem=strict, PrivateTmp=, NoNewPrivileges= and the rest work best inside system unit, because some of dem need namespace and capability wey unprivileged user manager no get. The sandboxing guide and the memory and CPU limit guide show how to set dem there.

For everything else, the Telegram bot, the n8n on port 5678, the Ollama on 11434, the coding agent wey dey watch one repo, systemctl --user plus linger na enough, and you no go ever touch /etc/systemd/system/.

FAQ

Why my systemd user service dey stop when I logout?

Because logind dey stop your user manager, user@UID.service, about 10 seconds after your last session close, and every user service dey live under that manager. Run sudo loginctl enable-linger yourname one time. After that, logind go start the user manager at boot and leave am running when you logout. Check with loginctl show-user yourname -p Linger; e suppose print Linger=yes.

Why systemctl --user dey fail with bus error inside sudo -u or su?

Because systemctl --user need XDG_RUNTIME_DIR to find the bus socket at /run/user/UID/bus, and su or sudo -u no set am, since those two no create logind session. From root, use sudo systemctl --user -M username@ status, wey open proper session for that user and reach im manager. Or SSH in directly as that user.

My user service dey start by hand but no dey start after reboot. Wetin dey wrong?

Two usual causes. Either linger no dey on, so the user manager itself no start till you login, or your [Install] section say WantedBy=multi-user.target, wey no exist inside the user manager, so enable create symlink wey nothing ever follow. Change am to WantedBy=default.target, run systemctl --user daemon-reload, then systemctl --user reenable yourservice.service.

User service fit listen on port 80?

No. Unprivileged user no fit bind port below 1024, and the user manager no fit hand out CAP_NET_BIND_SERVICE. Either run the service on high port like 8080 and put nginx in front, or write system unit with User= and AmbientCapabilities=CAP_NET_BIND_SERVICE inside /etc/systemd/system/.

#systemd#systemctl#linux#vps#services#linger