Start here
Is it running
systemctl status nginxWhy did it stop
journalctl -u nginx -n 50 -xWhat is broken right now
systemctl --failedStart it and keep it after reboot
systemctl enable --now nginxI edited a unit file
systemctl daemon-reloadWhy is boot slow
systemd-analyze blame
Two words that are not the same. Active means running now. Enabled means it starts at boot. A service can be active and disabled, which is why it disappears after the next reboot, and that is the single most common systemd surprise for a Windows admin.
Controlling units
| Task | Command |
|---|---|
| Status, with the last log lines | systemctl status sshd |
| Start, stop, restart | systemctl start|stop|restart sshd |
| Reload configuration without dropping connections | systemctl reload nginx |
| Reload if the unit supports it, otherwise restart | systemctl reload-or-restart nginx |
| Enable at boot and start now | systemctl enable --now sshd |
| Disable at boot and stop now | systemctl disable --now sshd |
| Quick yes or no answers, useful in scripts | systemctl is-active sshd, systemctl is-enabled sshd |
| Make it impossible to start, even as a dependency | systemctl mask sshd |
| Undo that | systemctl unmask sshd |
| Clear the failed state after fixing something | systemctl reset-failed sshd |
disable is not stop.
systemctl disable only removes the boot symlink, leaving the service running until the next reboot. Use --now when you mean both, which is almost always.
Finding things
| Question | Command |
|---|---|
| Services loaded right now | systemctl list-units --type=service |
| Only the running ones | systemctl list-units --type=service --state=running |
| Everything installed, running or not | systemctl list-unit-files --type=service |
| What is enabled at boot | systemctl list-unit-files --state=enabled |
| I do not know the exact name | systemctl list-units --type=service | grep -i ssh |
| Where is this unit’s file | systemctl show -p FragmentPath sshd |
| Show the unit file and its drop-ins | systemctl cat sshd |
| What does this unit depend on | systemctl list-dependencies sshd |
| Every property systemd knows about it | systemctl show sshd |
journalctl
| Task | Command |
|---|---|
| Last 50 lines for one service | journalctl -u nginx -n 50 |
| Follow live | journalctl -u nginx -f |
| Errors and worse, system wide | journalctl -p err -b |
| This boot only | journalctl -b |
| The previous boot, for a crash | journalctl -b -1 |
| Which boots are recorded | journalctl --list-boots |
| A time window | journalctl --since "2026-09-16 08:00" --until "09:00" |
| Relative time | journalctl --since "1 hour ago" |
| Kernel messages | journalctl -k |
| Add explanations where they exist | journalctl -xe |
| Structured output for parsing | journalctl -u nginx -o json-pretty |
| How much disk the journal uses | journalctl --disk-usage |
| Trim it | journalctl --vacuum-size=500M or --vacuum-time=14d |
| Priority | Level |
|---|---|
0 to 2 | emerg, alert, crit |
3 | err |
4 | warning |
5 to 7 | notice, info, debug |
The journal may not survive a reboot. With
Storage=auto, which is the common default, logs are kept on disk only if /var/log/journal exists. If journalctl -b -1 returns nothing after a crash, create that directory and restart systemd-journald, then you will have history next time.
Unit files
| Location | Meaning |
|---|---|
/usr/lib/systemd/system | Shipped by packages. Do not edit, an update overwrites it. |
/lib/systemd/system | Same thing on the Debian family, where it is a symlink to the above |
/etc/systemd/system | Your overrides. Wins over the package version. |
/run/systemd/system | Runtime units, gone after reboot |
/etc/systemd/system/name.service.d/ | Drop-in directory: small files that change part of a unit |
| Task | Command |
|---|---|
| Change a setting safely, creating a drop-in | systemctl edit nginx |
| Copy the whole unit and edit that | systemctl edit --full nginx |
| Throw away your changes | systemctl revert nginx |
| See the effective unit, drop-ins included | systemctl cat nginx |
| After editing any file by hand | systemctl daemon-reload |
| Check a unit file for mistakes | systemd-analyze verify /etc/systemd/system/my.service |
Forgetting daemon-reload is the classic time sink. systemd keeps units in memory, so an edited file changes nothing until it is reloaded, and
systemctl restart happily starts the old definition. systemctl edit does the reload for you, which is the better reason to use it.
A service of your own
A script that must run as a managed service. Save as /etc/systemd/system/report.service, then systemctl daemon-reload and systemctl enable --now report.
[Unit]
Description=Nightly report job
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/report.sh
User=svc-report
WorkingDirectory=/opt/report
[Install]
WantedBy=multi-user.target
| Directive | What it controls |
|---|---|
Type=simple | Default. The process started by ExecStart is the service. |
Type=oneshot | Runs, finishes, and that is success. Right for scripts. |
Type=forking | The program daemonises itself. Usually needs PIDFile=. |
Type=notify | The program tells systemd when it is ready |
Restart=on-failure | Restart after a non-zero exit. Also always, no. |
RestartSec=10 | Wait before restarting |
User=, Group= | Run as a service account, not root |
Environment= | Set a variable, or use EnvironmentFile= |
After=, Wants= | Ordering and a soft dependency |
WantedBy=multi-user.target | What enable hooks it into |
The PATH is not your PATH. A unit runs with a minimal environment and no shell profile, so use absolute paths in
ExecStart and inside the script. This is the same trap as a PowerShell script that works by hand and fails in Task Scheduler.
Timers instead of cron
A timer is the scheduled task. It has the same name as the service it starts, with a .timer extension.
[Unit]
Description=Run the nightly report
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
| Task | Command or value |
|---|---|
| Enable the timer, not the service | systemctl enable --now report.timer |
| What is scheduled and when it next runs | systemctl list-timers --all |
| Test the schedule expression first | systemd-analyze calendar "Mon *-*-* 06:30:00" |
| Run the job now, without waiting | systemctl start report.service |
| Did it work | journalctl -u report.service -n 30 |
| Every day at 02:00 | OnCalendar=*-*-* 02:00:00 |
| Every Monday at 06:30 | OnCalendar=Mon *-*-* 06:30:00 |
| Every 15 minutes | OnCalendar=*:0/15 |
| Catch up after the machine was off | Persistent=true |
| Spread load across a fleet | RandomizedDelaySec=300 |
Why timers beat cron here. The output lands in the journal instead of a mail spool, the job inherits the unit’s user and environment,
Persistent=true handles a missed run, and list-timers answers “what runs on this box” in one line. cron still works and is fine for something trivial.
Boot, targets and timing
| Task | Command |
|---|---|
| How long did the last boot take | systemd-analyze time |
| What took the longest | systemd-analyze blame |
| What actually delayed the chain | systemd-analyze critical-chain |
| Current default target | systemctl get-default |
| Boot to console instead of a desktop | systemctl set-default multi-user.target |
| Switch target now | systemctl isolate multi-user.target |
| Reboot and power off | systemctl reboot, systemctl poweroff |
| Target | Windows equivalent, roughly |
|---|---|
multi-user.target | Server Core: full system, no graphical session |
graphical.target | Desktop Experience |
rescue.target | Safe mode, single user |
emergency.target | Barely anything mounted, for repair |
blame lists the slowest units, not the cause. A unit can be slow because it waited for something else.
critical-chain shows the ordering, which is what you need when boot takes four minutes and the culprit is a network mount that nobody can reach.
Distribution notes
| Detail | Where it differs |
|---|---|
| SSH unit name | sshd on the RHEL family and Photon OS, ssh on Debian and Ubuntu |
| Web server unit name | httpd on the RHEL family, apache2 on Debian and Ubuntu |
| Time sync unit | chronyd on the RHEL family, systemd-timesyncd or chrony on Ubuntu |
| Firewall unit | firewalld on the RHEL family, ufw on Ubuntu |
| Plain log files, if you prefer them | /var/log/messages on the RHEL family, /var/log/syslog on Debian and Ubuntu |
| Journal persistence by default | Usually on with the RHEL family, often volatile on minimal Debian and appliance images |
| vCenter Server Appliance | Photon OS, systemd throughout. service-control --status --all is the VMware wrapper around the same units. |
| ESXi | No systemd at all. Services are /etc/init.d/hostd restart and friends. |
| Very old systems | RHEL 6 and Ubuntu 14.04 predate systemd: use service name status and chkconfig. |
FAQ
The service starts by hand and dies at boot.
Usually a dependency that is not ready yet, most often the network or a mount. Add
After=network-online.target together with Wants=network-online.target, and for a mounted path add RequiresMountsFor=. journalctl -b -u name shows the difference between the two attempts.
systemctl status shows active, but the application is not working.
With
Type=simple, systemd reports active as soon as the process starts, not when it is ready to serve. That is what Type=notify exists for. Until then, check the application’s own log rather than trusting the unit state.
My change to the unit file does nothing.
Either you edited the packaged copy under
/usr/lib/systemd/system while an override in /etc/systemd/system takes precedence, or you skipped systemctl daemon-reload. systemctl cat name shows what systemd is actually using.
A service keeps restarting in a loop.
Restart=always with a program that exits immediately. systemd applies a rate limit and then gives up, leaving the unit failed. Fix the program, or set StartLimitIntervalSec and StartLimitBurst deliberately, and clear the state with systemctl reset-failed.
How do I see everything that happened around a crash?
journalctl -b -1 -p err for errors in the previous boot, then widen with a time window around the timestamp. If the previous boot is not there, the journal is volatile on this machine, and creating /var/log/journal fixes that for next time.