Most systemctl cheat sheets stop at start, stop and enable, and they miss the systemd changes that now bite on a stock server. A root systemctl reboot now refuses to run while a backup holds an inhibitor lock, and set-property writes a permanent drop-in unless you ask for --runtime. On top of that, soft-reboot restarts userspace without touching the kernel, and is-active returns 4 for a unit that does not exist.
This systemctl cheat sheet groups the commands by task, from service control and boot targets to drop-ins, timers, journal lookups and the error messages you’ll actually see. A quick-find table comes first, so you can jump straight to systemctl edit or anything else. Every command shown with output was run in October 2026 on Ubuntu 26.04 (systemd 259) and Rocky Linux 10.2 (systemd 257). Most output comes from the Ubuntu box, with every Rocky difference called out.
Find the systemctl command you need
Start here if you know the job but not the verb. Each row is covered in a section further down, most of them with real output, and UNIT stands for the service name, such as nginx:
| Task | Command |
|---|---|
| Start, stop, restart, reload | sudo systemctl start|stop|restart|reload UNIT |
| Start at boot and start now | sudo systemctl enable --now UNIT |
| Check status and recent logs | systemctl status UNIT --no-pager |
| Scripted health check | systemctl is-active --quiet UNIT |
| One property, value only | systemctl show UNIT -P MainPID |
| Change a setting safely | sudo systemctl edit UNIT or edit --drop-in=NAME --stdin UNIT |
| Replace the whole unit file | sudo systemctl edit --full UNIT |
| Undo every local change | sudo systemctl revert UNIT |
| Show the unit with drop-ins | systemctl cat UNIT |
| Cap memory or CPU | sudo systemctl set-property UNIT MemoryMax=256M |
| Block a unit from starting | sudo systemctl mask --now UNIT |
| List failed units | systemctl --failed |
| Latest logs for a unit, explained | journalctl -xeu UNIT |
| Reboot later, or cancel it | sudo systemctl reboot --when="+30min", --when=cancel |
| Restart userspace only | sudo systemctl soft-reboot |
| Manage another user’s services | sudo systemctl --user -M alice@ status |
Check your systemd version and distro defaults
systemd changes something you’d type in almost every release, so check the version before you trust any example, including ours. systemctl --version prints it on the first line:
systemctl --version | head -1; grep PRETTY /etc/os-release; uname -r
Ubuntu 26.04 reports 259, and the Rocky box reports 257 for the same command. The gap is two releases, so anything marked 258 or 259 later on this page works on Ubuntu only, and a playbook that targets both families has to stick to the 257 feature set:
systemd 259 (259.9-0ubuntu0.1)
PRETTY_NAME="Ubuntu 26.04.1 LTS"
7.0.0-15-generic
Beyond the version, the two distros differ in defaults that change what you see, and most cheat sheets assume only one of them. This is what our two fresh cloud images did:
| Ubuntu 26.04 | Rocky Linux 10.2 | |
|---|---|---|
| systemd | 259 | 257 |
| sudo | sudo-rs 0.2.13 (classic sudo also installed) | sudo 1.9.17p2 |
| nginx after package install | enabled and running | disabled and stopped |
| Vendor unit path | /usr/lib/systemd/system/ | /usr/lib/systemd/system/ |
| Default target (cloud image) | graphical.target | multi-user.target |
enable output | SysV sync lines, then Created symlink | Created symlink only |
| SysV and rc-local generators | still shipped | still shipped |
start -v (systemd 258+) | works | invalid option -- 'v' |
| Polkit refusal wording | Access denied as the requested operation requires interactive authentication... | Interactive authentication required. |
The SysV row has an expiry date. systemd 260 removed SysV init script support and rc-local upstream, so an /etc/init.d script that still works on both of these releases stops loading on the next distro version that moves past 259. Convert those scripts to units now, while the generator still shows you what each script did (systemctl cat prints the generated unit). The same 259 versus 257 gap is why start -v works on Ubuntu and not on Rocky, and why the polkit and inhibitor errors later on read differently on each box.
Start, stop, restart and reload services
The lifecycle verbs need root because they change system state, so prefix them with sudo (or run0, covered in the errors section). restart stops and starts the process. reload asks the daemon to reread its config without dropping connections, and it only works when the unit defines ExecReload:
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx
sudo systemctl reload nginx
Pick the verb by what the change needs, because a restart drops every open connection:
reloadafter a config edit, when the unit hasExecReloadrestartafter a binary upgrade or an environment changetry-reload-or-restartin config-management hooks, since it reloads if it can, restarts if it cannot, and skips a stopped unit
Both reload and try-reload-or-restart exited 0 on our nginx, and that exit code is all a script sees. It means systemd accepted and ran the job, so confirm the service is in the state you expect with status, which also shows the last few log lines:
systemctl status nginx --no-pager
On Ubuntu 26.04 the header now carries an Invocation: ID and a memory peak, both newer than most examples online. --no-pager stops systemctl from opening less in scripts:
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-10-21 23:16:01 UTC; 4s ago
Invocation: 19390cce3b944f1fa8ad2e16f03992ba
Docs: man:nginx(8)
Main PID: 30019 (nginx)
Tasks: 3 (limit: 3520)
Memory: 3M (peak: 7M)
CPU: 41ms
CGroup: /system.slice/nginx.service
├─30019 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
├─30021 "nginx: worker process"
└─30022 "nginx: worker process"
Oct 21 23:16:01 web01 systemd[1]: Starting nginx.service - A high performance web server and a reverse proxy server...
Oct 21 23:16:01 web01 systemd[1]: Started nginx.service - A high performance web server and a reverse proxy server.
Read preset: carefully. It is the distro’s default for that unit, not its current state, so Rocky shows enabled; preset: disabled after you enable nginx yourself. That mismatch is not an error, but systemctl preset nginx would put it back to disabled, so be careful running preset commands across a fleet. To signal a process directly, systemctl kill sends to every process in the unit by default, and --kill-whom=main limits it to the main PID:
sudo systemctl kill --kill-whom=main -s HUP nginx
For nginx both forms acted as a reload on both distros. The master kept its PID and the workers came back with new ones, so systemctl reload remains the clearer choice for nginx and --kill-whom=main is for daemons whose workers die on a stray HUP. Check pgrep -a NAME before and after any signal you send this way, because for nginx-style daemons a worker PID that changes is the simplest proof the reload happened.
Enable services at boot, or mask them
enable creates the symlink that starts a unit at boot, and --now starts it in the same command. On Rocky, where nginx arrives disabled, this is the whole output:
sudo systemctl enable --now nginx
Newer systemd quotes both paths in the symlink line, so a script that parses the old unquoted form needs a small change. The line only appears when a link is actually created, which is why an already-enabled unit prints nothing here:
Created symlink '/etc/systemd/system/multi-user.target.wants/nginx.service' → '/usr/lib/systemd/system/nginx.service'.
Ubuntu prints two extra lines on every enable and disable for packages that also ship an /etc/init.d script, because it keeps the SysV links in step. Our disable and enable pair on Ubuntu:
sudo systemctl disable nginx; sudo systemctl enable nginx
The SysV lines come first each time. They are harmless, but they explain why Ubuntu’s output differs from every Rocky example you’ll find, and they disappear once a package drops its init script:
Synchronizing state of nginx.service with SysV service script with /usr/lib/systemd/systemd-sysv-install.
Executing: /usr/lib/systemd/systemd-sysv-install disable nginx
Removed '/etc/systemd/system/multi-user.target.wants/nginx.service'.
Synchronizing state of nginx.service with SysV service script with /usr/lib/systemd/systemd-sysv-install.
Executing: /usr/lib/systemd/systemd-sysv-install enable nginx
Created symlink '/etc/systemd/system/multi-user.target.wants/nginx.service' → '/usr/lib/systemd/system/nginx.service'.
disable only stops automatic starts; anything can still start the unit by hand or as a dependency. mask links the unit to /dev/null so nothing can start it, and --now stops it as well:
sudo systemctl mask --now nginx
The output is one symlink pointing at /dev/null. Any later start fails until you unmask it, which is exactly the point on a box where a package or a dependency keeps pulling a service back in:
Created symlink '/etc/systemd/system/nginx.service' → '/dev/null'.
List what is masked before you blame a service for not starting. Ubuntu 26.04 masks six units out of the box, while Rocky masked only the one we did. This is the Ubuntu list with our nginx mask still in place, which makes seven:
systemctl list-unit-files --state=masked --no-pager
Six of the seven are distro decisions, so leave them alone unless you know why they were masked. The classic sudo package ships sudo.service as a link to /dev/null (dpkg -S names it as the owner), and units like hwclock.service are masked for the same reason: an old init script with no systemd job to do. That is why the only line to act on here is nginx:
UNIT FILE STATE PRESET
cryptdisks-early.service masked enabled
cryptdisks.service masked enabled
hwclock.service masked enabled
nginx.service masked enabled
screen-cleanup.service masked enabled
sudo.service masked enabled
x11-common.service masked enabled
7 unit files listed.
sudo systemctl unmask nginx removes the link (Removed '/etc/systemd/system/nginx.service'.), and a normal start works again. Boot targets use the same model: systemctl get-default shows the target the box boots into, sudo systemctl set-default multi-user.target changes it for the next boot. isolate switches the running system to another target immediately, which the manual calls dangerous for good reason: isolating rescue.target on a remote server stops SSH and networking, so only do it from a console.
Inspect units and script against them
For scripts, the exit code is the answer and the text is noise. These are the codes both distros returned, and code 4 for a missing unit is newer than most scripts that test for it:
systemctl is-active nginx; echo "exit=$?"; systemctl is-active nosuch.service; echo "exit=$?"; systemctl is-enabled nosuch.service; echo "exit=$?"
A missing unit prints inactive from is-active, the same word as a stopped service, so test the exit code rather than the word. Code 4 is how a script tells a typo in the unit name apart from a service that is simply down:
active
exit=0
inactive
exit=4
not-found
exit=4
The codes are the same on both distros, and each verb has its own meaning for them:
| Exit code | From | Means |
|---|---|---|
| 0 | is-active, is-enabled | Active, or enabled |
| 1 | is-failed | Not in the failed state |
| 3 | is-active | Known unit, not running |
| 4 | is-active, is-enabled, status | No such unit |
| 5 | start | No such unit (Unit nosuch.service not found.) |
For a quiet check, --quiet drops the text so only the exit code is left. That is the form for health checks and && chains, because nothing printed can change between releases and break a comparison:
systemctl is-active --quiet nginx && echo "nginx is up"
Use show when a script needs a value instead of a yes or no. -P prints one property’s value with no Key= prefix, and -p takes a comma-separated list:
systemctl show nginx -P MainPID; systemctl show nginx -p ActiveState,SubState,NRestarts
The PID comes back bare, then one line per property. Don’t rely on the order: Rocky printed NRestarts first for the same command:
30019
ActiveState=active
SubState=running
NRestarts=0
The listing verbs cover the rest of the inventory. Add --legend=no --no-pager to drop the header and footer when you pipe the output:
systemctl list-units --type=service --state=runningfor what is running nowsystemctl list-unit-files --type=service --state=enabledfor what starts at bootsystemctl --failedfor failed units, andsystemctl is-system-runningfor the one-word summarysystemctl list-timers,list-paths,list-automountsfor the other unit typessystemctl list-dependencies --reverse nginxfor what pulls a unit insystemctl cat nginxfor the unit file plus every drop-in, with pathssystemctl whoami $$for the unit a PID belongs to (our shell wassession-12.scope)
is-system-running printed running on both boxes, and degraded in that spot means at least one unit failed. Mistype a verb and systemd suggests the right one: systemctl stauts nginx answered Unknown command verb 'stauts', did you mean 'status'? with exit 1.
Change unit settings with drop-ins
Never edit files under /usr/lib/systemd/system/, because the next package update overwrites them. Put changes in a drop-in under /etc/systemd/system/UNIT.d/ instead. sudo systemctl edit nginx opens an editor on override.conf and reloads systemd when you save, and --full copies the whole unit to /etc when you really need to replace it. For automation, --stdin takes the content from a pipe and --drop-in= names the file, so several tools can each own a separate drop-in instead of fighting over one override.conf:
printf '[Service]\nLimitNOFILE=65536\n' | sudo systemctl edit --stdin --drop-in=limits nginx
It writes the file and reloads systemd in one step, so there is no editor, no temp file and no forgotten daemon-reload. That makes it the form to use from Ansible, cloud-init or a shell script:
Successfully installed edited file '/etc/systemd/system/nginx.service.d/limits.conf'.
Confirm the drop-in landed and the new value is live. systemctl cat ends with the drop-in and show -P reads the effective setting:
systemctl cat nginx --no-pager | tail -4; systemctl show nginx -P LimitNOFILE
The file path is printed as a comment above its content, which is how you find an override someone else left behind. The last line is the effective value, so the drop-in is live:
# /etc/systemd/system/nginx.service.d/limits.conf
[Service]
LimitNOFILE=65536
65536
If you write a drop-in by hand instead, systemd does not see it until you reload, and every status nags you in the meantime. We added a Nice=5 file with tee and checked status straight away:
printf '[Service]\nNice=5\n' | sudo tee /etc/systemd/system/nginx.service.d/nice.conf >/dev/null; systemctl status nginx --no-pager | head -4
The warning comes before the status header, and the drop-in list underneath is stale because systemd has not read the new file yet:
Warning: The unit file, source configuration file or drop-ins of nginx.service changed on disk. Run 'systemctl daemon-reload' to reload units.
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Drop-In: /etc/systemd/system/nginx.service.d
└─limits.conf
The Drop-In: list still showed only limits.conf until sudo systemctl daemon-reload ran. To audit every override on the host, systemd-delta --type=extended lists each drop-in against the unit it extends. Ubuntu 26.04 showed ten, eight of them shipped by the distro itself. revert, which removes every local change in one go, comes at the end of the next section, after the resource limits have added files of their own.
Set resource limits with set-property
set-property changes cgroup limits on a running unit without a restart, and the change is permanent by default. The previous version of this page said the opposite. Without --runtime it writes a drop-in under /etc, which survives reboots:
sudo systemctl set-property nginx MemoryMax=256M; ls /etc/systemd/system.control/nginx.service.d/
The file name records the property it sets, so ls on that folder is a quick audit of every limit applied this way:
50-MemoryMax.conf
Add --runtime for a temporary change. It goes to /run, which is cleared on reboot:
sudo systemctl set-property --runtime nginx CPUQuota=50%; ls /run/systemd/system.control/nginx.service.d/
It uses the same naming in a different tree. Because /run is a tmpfs, the limit disappears at the next boot without any cleanup:
50-CPUQuota.conf
Both limits applied immediately. Read them back with show, together with the two drop-in settings from the previous section, which were still in place at this point:
systemctl show nginx -p MemoryMax,CPUQuotaPerSecUSec,LimitNOFILE,Nice
systemd reports the limits in the units it stores, so 256M comes back as bytes and 50% as CPU time per second. Compare the numbers rather than the strings you typed when you script a check:
CPUQuotaPerSecUSec=500ms
MemoryMax=268435456
LimitNOFILE=65536
Nice=5
When the experiment is over, revert removes every local change to the unit in one go, including the permanent MemoryMax limit:
sudo systemctl revert nginx
It took both drop-ins from the previous section and the two resource-limit files from this one. Nothing under /usr/lib was touched, so the unit falls back to exactly what the package shipped:
Removed '/etc/systemd/system.control/nginx.service.d/50-MemoryMax.conf'.
Removed '/etc/systemd/system.control/nginx.service.d'.
Removed '/run/systemd/system.control/nginx.service.d/50-CPUQuota.conf'.
Removed '/run/systemd/system.control/nginx.service.d'.
Removed '/etc/systemd/system/nginx.service.d/nice.conf'.
Removed '/etc/systemd/system/nginx.service.d/limits.conf'.
Removed '/etc/systemd/system/nginx.service.d'.
After the revert, LimitNOFILE was back to the default of 524288 and Nice to 0, with no daemon-reload needed. That makes revert the cleanest rollback after an experiment, because it clears /etc and /run overrides together. Deleting drop-in folders by hand misses the system.control files that set-property writes.
Live resource views come from systemd-cgtop (top for cgroups) and systemd-cgls (the process tree per unit). For a one-off job with limits, sudo systemd-run --scope -p MemoryMax=512M COMMAND wraps any command in a transient unit.
Freeze a service without stopping it
freeze suspends every process in the unit through the cgroup freezer, so nothing is killed and thaw resumes exactly where it stopped. It’s handy for taking a consistent snapshot or pausing a runaway job while you look at it:
sudo systemctl freeze nginx; systemctl status nginx --no-pager | head -3; curl -s -m 3 -o /dev/null -w "http=%{http_code}\n" http://localhost/; sudo systemctl thaw nginx; systemctl status nginx --no-pager | head -3 | tail -1
Status gains a (frozen) flag, and an HTTP request timed out after three seconds with no response, because the workers couldn’t run. After thaw, status showed it running again with its original start time, so nothing was restarted, and a second request in a later run returned thawed http=200:
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) (frozen) since Wed 2026-10-21 23:17:33 UTC; 2s ago
http=000
Active: active (running) since Wed 2026-10-21 23:17:33 UTC; 5s ago
A frozen web server holds connections open without answering them, so freeze only what you will thaw within seconds. The cleanup verb is clean, which deletes a stopped unit’s directories when the unit declares them. Plain clean removes cache, runtime and file-descriptor-store data, while --what=all also deletes the state, logs and configuration directories, which for a database is the data itself. nginx’s unit declares none, so sudo systemctl clean --what=all nginx answered Failed to clean unit nginx.service: No matching resources found. and exit 1.
Write a service and a timer
A oneshot service plus a timer replaces a cron line, and you get journal logs and dependency ordering with it. Create the service first:
sudo vim /etc/systemd/system/backup-demo.service
This is the unit we ran, a nightly tarball of /etc. Type=oneshot makes systemd wait for the command to finish before it treats the unit as started, so systemctl start blocks until the tarball is written and anything ordered after it waits too:
[Unit]
Description=Nightly backup demo
[Service]
Type=oneshot
ExecStart=/usr/bin/tar -czf /var/tmp/etc-backup.tgz /etc
Then the timer, with the same base name. systemd pairs a timer with the service of the same name unless you set Unit=, so the names have to match:
sudo vim /etc/systemd/system/backup-demo.timer
Persistent=true runs a missed job at the next boot, and RandomizedDelaySec spreads a fleet of servers over 15 minutes instead of hitting the backup target at once:
[Unit]
Description=Run backup-demo nightly
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=15min
[Install]
WantedBy=timers.target
Reload, then enable the timer, not the service. The service has no [Install] section on purpose, because the timer is what starts it:
sudo systemctl daemon-reload; sudo systemctl enable --now backup-demo.timer
list-timers confirms the next run, already shifted by the random delay. Check it after every change to a timer, because a typo in OnCalendar= can leave the timer loaded with no next run:
systemctl list-timers backup-demo.timer --no-pager
Rocky picked 02:00:28 and Ubuntu 02:13:57 for the same timer, which is the random delay doing its job. This is the Ubuntu listing:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2026-10-22 02:13:57 UTC 2h 54min - - backup-demo.timer backup-demo.service
1 timers listed.
Pass --all to see loaded but inactive timers, too.
Test the job without waiting for 2 AM with sudo systemctl start backup-demo.service, then check systemctl show backup-demo.service -p Result,ExecMainStatus for Result=success. Before you commit to a schedule, systemd-analyze calendar "Mon..Fri 02:00" prints the normalized form and the next time it fires. If the job fails, systemctl list-timers still shows the next run, so check Result= or the journal rather than the timer. Our systemd timers guide covers monotonic timers and calendar syntax in depth, and the rsync backup timer is a production version of this pattern.
Read a unit’s logs with journalctl
systemctl’s own error messages tell you to run journalctl -xeu UNIT: -u filters by unit, -e jumps to the end, and -x adds the explanatory catalog text. The newer trick is invocations. Each start of a unit gets an ID, and you can list them:
journalctl -u nginx --list-invocations --no-pager | tail -4
This list is from the Ubuntu box. The newest run is 0 and older ones count down, and -2 is the failed start from the broken-config test in the errors section below. A run whose start and end times match is worth a look, so this list is a quick way to spot one:
-3 f065954dc65341e6b4cc8fe807696e83 Wed 2026-10-21 23:18:07 UTC Wed 2026-10-21 23:18:07 UTC
-2 b775c798bba04dbba35f9678ddc5d9c1 Wed 2026-10-21 23:18:07 UTC Wed 2026-10-21 23:18:07 UTC
-1 3b01ce19c8bc4079badae4652519f749 Wed 2026-10-21 23:18:07 UTC Wed 2026-10-21 23:18:38 UTC
0 e6e3daa43d934a7386d5411ed95055f5 Wed 2026-10-21 23:18:39 UTC Wed 2026-10-21 23:18:39 UTC
Pick a run with --invocation=-1. The short form trips people up: journalctl -u nginx -I -1 failed on both distros with journalctl: invalid option -- '1', because -I on its own already means the latest run. Use the long form for older runs:
journalctl -u nginx --invocation=-1 --no-pager | tail -3
On Ubuntu that run was a clean stop before a test restart, while the same command on Rocky returned its failed start, because the run order differed between the boxes. Reading it took one command instead of scrolling through timestamps, so it is the fastest way to answer what happened the last time a service went down:
Oct 21 23:18:38 web01 systemd[1]: Stopping nginx.service - A high performance web server and a reverse proxy server...
Oct 21 23:18:38 web01 systemd[1]: nginx.service: Deactivated successfully.
Oct 21 23:18:38 web01 systemd[1]: Stopped nginx.service - A high performance web server and a reverse proxy server.
A few more journalctl filters cover most service debugging, and they combine with -u freely:
-bfor this boot and-b -1for the previous one-p errfor errors and worse--since "1 hour ago"for a time window-fto follow new lines as they arrive
On systemd 258 and later there is a shortcut for the start itself: sudo systemctl start -v UNIT streams the unit’s log lines while the job runs. Ubuntu printed the Starting and Started lines inline, and Rocky rejected the flag because 257 predates it. The journalctl filtering guide covers the rest, and persistent journal storage keeps logs across reboots.
Schedule, block and soft-reboot
Since systemd 257, a block inhibitor stops root too. Before that, root’s reboot ignored inhibitor locks; now a backup tool or package manager holding one makes the reboot fail, so an unattended patch run that ends in a reboot needs to check for locks first.
We held a lock with systemd-inhibit, wrapped in a transient unit by systemd-run so it can be released by name later without touching any other inhibitor on the box:
sudo systemd-run --unit=inhibit-demo systemd-inhibit --what=shutdown --mode=block --who=backup --why="nightly backup running" sleep 600; sleep 2; systemd-inhibit --list --no-pager | grep -E "WHO|block"
Confirm the block row is there before you test a reboot, because without it the next command really reboots the machine:
Running as unit: inhibit-demo.service; invocation ID: 8e246b29727642d399f5baf91f323dd5
WHO UID USER PID COMM WHAT WHY MODE
backup 0 root 2092 systemd-inhibit shutdown nightly backup running block
At an interactive terminal, sudo systemctl reboot then refuses before it asks logind for anything, names the lock and exits 1. Both distros printed the same text, and Rocky listed the logged-in SSH user twice:
Operation inhibited by "backup" (PID 2092 "systemd-inhibit", user root), reason is "nightly backup running".
User josphat is logged in on sshd.
Please retry operation after closing inhibitors and logging out other users.
'systemd-inhibit' can be used to list active inhibitors.
Alternatively, ignore inhibitors and users with 'systemctl reboot -i'.
Scripts, cron jobs and Ansible tasks have no terminal, so they skip that friendly check and get logind’s answer instead, which differs by distro and is the string to search your job logs for:
- Ubuntu:
Call to Reboot failed: Operation denied due to active block inhibitor - Rocky:
Call to Reboot failed: Access denied
The Rocky message doesn’t mention the inhibitor at all, so check systemd-inhibit --list whenever a root reboot in a script says access denied. Once you’ve confirmed the job holding the lock can be interrupted, sudo systemctl reboot -i ignores inhibitors and logged-in users for that one reboot, as the message itself says. The -i flag did not help a scheduled reboot, though, which was refused the same way. Release the test lock by stopping its unit:
sudo systemctl stop inhibit-demo
Without an inhibitor, scheduling works and prints its own cancel hint. This schedules a real reboot, so cancel it afterwards if you are only testing:
sudo systemctl reboot --when="+30min"
The time is shown in UTC because these cloud images run in UTC, so convert before you promise a maintenance window to anyone. The reboot then happens without anyone logged in, which is the point of scheduling it:
Reboot scheduled for Wed 2026-10-21 23:48:51 UTC, use 'systemctl reboot --when=cancel' to cancel.
The other --when forms manage that schedule, and the exit code tells a script whether anything is pending:
systemctl reboot --when=showrepeats the scheduled linesudo systemctl reboot --when=cancelclears it--when=showwith nothing pending printsNo scheduled shutdown.and exits 1--when="$(date -d 'tomorrow 02:00' '+%F %T')"schedules a clock time on a full date; a time of day alone means today, and a time that has already passed fires immediatelypoweroffandhaltaccept the same flag
soft-reboot restarts all of userspace on the running kernel, which skips firmware, bootloader and kernel init. We timed both kinds of reboot from another machine, measuring from the command until SSH answered again. The script first waited for SSH to stop answering, which took under a second every time, so the numbers are real restarts:
| Ubuntu 26.04 | Rocky Linux 10.2 | |
|---|---|---|
sudo systemctl soft-reboot | 7.6 s | 3.4 s |
sudo systemctl reboot | 16.8 s | 12.1 s |
uptime -s after soft-reboot | unchanged | unchanged |
systemctl show -p SoftRebootsCount | 1, back to 0 after the full reboot | 1, back to 0 after the full reboot |
Use it after userspace updates (glibc, systemd, OpenSSL) when the kernel didn’t change, and use a full reboot for kernel updates, because soft-reboot keeps the old kernel running. The saving is real on both distros, but most of a full reboot on a VM is firmware and kernel init, so the gap grows on physical servers with long POST times.
Manage user services
Each logged-in user gets a systemd manager of their own, controlled with systemctl --user. It runs without root, and unit files go in ~/.config/systemd/user/. The common trap is reaching it after su or sudo -i, which do not set up the user’s session bus:
id alice >/dev/null 2>&1 || sudo useradd -m alice; sudo su - alice -c 'systemctl --user status'
The error names the two missing variables and suggests the fix, because su switches the user without starting a login session:
Failed to connect to user scope bus via local transport: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined (consider using --machine=<user>@.host --user to connect to bus of other user)
Turn on lingering so alice’s manager runs without a login, then reach it from root with -M alice@. The @ suffix tells systemctl to connect to that user’s manager on the local host, so no password or session for alice is needed:
sudo loginctl enable-linger alice; sleep 2; sudo systemctl --user -M alice@ is-system-running; sudo systemctl --user -M alice@ list-units --type=target --no-pager --legend=no | head -4
The user manager answers like the system one, with its own targets, and nothing here needed alice to log in:
running
basic.target loaded active active Basic System
default.target loaded active active Main User Target
paths.target loaded active active Paths
sockets.target loaded active active Sockets
Lingering also keeps user services running after logout, which is what rootless containers need. The rootless systemd services guide walks through a full user unit, and running containers as systemd services applies it to Podman and Docker.
Analyze boot time and unit security
systemd-analyze answers why a box boots slowly and how exposed a service is. systemd-analyze blame sorts units by start time, and critical-chain shows the path that actually delayed a target or unit. On both first boots, cloud-final.service took about three minutes, which is cloud-init doing first-boot work rather than anything systemctl can fix. security scores a unit’s sandboxing from 0 (locked down) to 10, and it reads the unit as loaded, drop-ins included, so you can rerun it after each hardening change to see the score move:
systemd-analyze security nginx.service --no-pager | tail -2
Stock nginx is close to wide open on Ubuntu, which is normal for a packaged daemon that has to work everywhere. Rocky’s unit, which adds PrivateTmp=true, scored 9.2, so the vendor files differ even for the same software:
→ Overall exposure level for nginx.service: 9.6 UNSAFE :-{
The full report lists every directive that would lower the score, and each one goes in a drop-in, so you can harden a service without touching the vendor file. Before you load a unit you wrote, check it for typos:
systemd-analyze verify /etc/systemd/system/demo.service
Our test unit, demo.service, had ExecStart=echo hello and a misspelled Restrat=always in its [Service] section. systemd only logs a warning for unknown keys and still loads the unit, so the typo is easy to miss. verify catches it, but still exits 0, so read its output in CI rather than trusting the exit code. Rocky printed just this line:
/etc/systemd/system/demo.service:6: Unknown key 'Restrat' in section [Service], ignoring.
Ubuntu’s run added two lines about CPUAccounting= in its own xfs_scrub units, a warning about distro files that you can ignore. Linux Service Management Made Easy with systemd (paid link) (Donald Tevault, Packt, 420 pages) is the book we’d recommend for unit hardening, cgroups and timers. It came out in 2022, so it predates soft-reboot, --when and the inhibitor change covered here.
Fix the errors systemctl prints
Each of these came from our two lab servers, and where Ubuntu and Rocky word the same failure differently, both strings are here. Search the page for the first words of yours, because the exact text is what tells you which fix applies.
Error: “Job for nginx.service failed because the control process exited with error code”
The service’s own start command failed, so systemd is only the messenger. We appended a bogus directive to nginx.conf and restarted:
Job for nginx.service failed because the control process exited with error code.
See "systemctl status nginx.service" and "journalctl -xeu nginx.service" for details.
Do what the second line says. journalctl -xeu nginx.service shows the program’s own error above systemd’s catalog text, and for nginx sudo nginx -t names the bad line. Fix the config, then start the service again.
Error: “Failed to start nginx.service: Unit nginx.service is masked.”
Someone or something masked the unit, so systemd refuses to start it no matter who asks. Check with systemctl list-unit-files --state=masked, then sudo systemctl unmask nginx and start it. If a package or a configuration tool masks it again, find out why before fighting it, because a mask is usually deliberate.
Error: “Unit nosuch.service could not be found.”
That’s status (exit 4); start says Failed to start nosuch.service: Unit nosuch.service not found. (exit 5). Usually it’s a typo or a distro naming difference such as ssh versus sshd, cron versus crond or apache2 versus httpd. systemctl list-unit-files 'ssh*' finds the real name. If you just wrote the unit file, run sudo systemctl daemon-reload.
Error: “Access denied as the requested operation requires interactive authentication”
On Ubuntu 26.04 this appears when a non-root user runs a state-changing verb with no way to ask for a password; Rocky says Interactive authentication required. for the same thing. Prefix the command with sudo, or use run0, systemd’s sudo replacement that runs commands through polkit. In a script with no terminal, run0 --no-ask-password failed with the same message, so scripts still need sudo or a polkit rule.
Error: “Failed to connect to user scope bus via local transport”
systemctl --user was run from a shell that has no user session, typically after su, so there is no runtime directory and no bus to talk to. Log in as the user over SSH, or from root use sudo systemctl --user -M alice@ ... with lingering enabled, as shown in the user services section. Restarting D-Bus, which some guides suggest, does nothing for this error.
Error: “System has not been booted with systemd as init system (PID 1). Can’t operate.”
systemctl is talking to a system where systemd isn’t PID 1, which means a container, a chroot or WSL without systemd. Running systemctl status inside a Podman container on Rocky printed it, followed by Failed to connect to system scope bus via local transport: Host is down. In containers, run the daemon in the foreground instead. On WSL, set systemd=true under [boot] in /etc/wsl.conf and restart WSL.
Error: “Call to Reboot failed: Operation denied due to active block inhibitor”
A process holds a block inhibitor, and since systemd 257 that stops root too. Rocky reports the same situation as Call to Reboot failed: Access denied. Run systemd-inhibit --list, let the job finish, or reboot with -i once you know it’s safe.
Error: “Start request repeated too quickly.”
The unit crashed more than StartLimitBurst times (5) within StartLimitIntervalSec (10 seconds) and systemd stopped restarting it. Our crash-loop test unit logged that line in the journal while systemctl status still showed Result: exit-code, so look in journalctl -u UNIT for it. Fix the crash, then sudo systemctl reset-failed UNIT before starting it again. If a service legitimately needs more restarts, raise StartLimitBurst= in a drop-in under [Unit] instead of setting Restart=always with no delay, because a unit that restarts in a tight loop also floods the journal.
Warning: “The unit file, source configuration file or drop-ins of nginx.service changed on disk”
You or a tool edited a unit file without reloading, so systemd is still running the old definition. Run sudo systemctl daemon-reload. Using systemctl edit instead of an editor avoids the warning, because it reloads for you.
Warning: “Stopping ‘ssh.service’, but its triggering units are still active”
Ubuntu 26.04 starts SSH through ssh.socket, so stopping only the service leaves the socket listening, and the next connection starts it again. That’s why a stopped service sometimes comes back. Stop or mask the .socket unit as well when you really want it gone.
Check which systemctl features your systemd has
Most of the newer verbs on this page arrived between systemd 252 and 259, and Rocky stops at the 257 rows, while Ubuntu 26.04 has everything up to 259. Older long-term releases such as Ubuntu 24.04 (systemd 255) miss the newest rows, so check this table against systemctl --version before copying a command into a playbook:
| Feature | Added in | Rocky 10.2 (257) | Ubuntu 26.04 (259) |
|---|---|---|---|
kill --kill-whom= | 252 | yes | yes |
edit --drop-in=, exit code 4 for unknown units | 253 | yes | yes |
soft-reboot, --when=, whoami, list-paths | 254 | yes | yes |
is-failed with no unit, verb typo suggestions | 255 | yes | yes |
run0, edit --stdin, Invocation: in status | 256 | yes | yes |
| Root reboot obeys block inhibitors, journalctl invocations | 257 | yes | yes |
start -v | 258 | no | yes |
| cgroup v1 support removed | 258 | already dropped by RHEL 10 | yes |
run0 --empower | 259 | no | yes |
| SysV scripts and rc-local removed | 260 | still supported | still supported |
The rows come from the systemd NEWS file and the systemctl manual, which tags each verb and flag with the version that added it. We ran the commands in the 252 to 257 rows on both servers (systemctl is-failed with no unit printed running and exit 1 on both), and the 258 row’s start -v showed the split in practice. The SysV row is the one to plan for: a fleet that still relies on /etc/init.d scripts will lose them on the first distro release that ships systemd 260 or later.
great (but long) writeup. Where is `systemctl edit`
It is in the “Change unit settings with drop-ins” section, and in the quick-find table at the top.
sudo systemctl edit nginx.servicecreates a drop-in override without touching the vendor file in /usr/lib/systemd/system/, andsudo systemctl edit --full nginx.servicereplaces the whole unit file.