RHEL Basics #4: Intro to systemd — Services, Targets, journalctl
In #3 we learned to install packages. Everything around them — starting them, stopping them, auto-starting on boot, reading their logs — happens on top of systemd. It’s been the standard since RHEL 7, and almost every modern Linux (Ubuntu included) uses the same model.
Where this post sits in the RHEL Basics series:
- #1 What is RHEL — From Fedora to RHEL, plus AlmaLinux and Rocky Linux
- #2 Setup — Installing RHEL 9, Subscription Manager, first login
- #3 dnf and package management — repo, modules, AppStream
- #4 Intro to systemd — services, targets, journalctl ← this post
- #5 Users / groups / permissions — UID/GID, sudo, ACL
- #6 Filesystem basics — XFS, mount, /etc/fstab
- #7 Basic security — firewalld, SSH hardening
What is systemd? #
After the kernel boots, the first user-space process to come up is PID 1. Long ago that was init (System V init); today it’s systemd on nearly every distro.
$ ps -p 1
PID TTY TIME CMD
1 ? 00:00:01 systemd
$ ls -l /sbin/init
lrwxrwxrwx. 1 root root ... /sbin/init -> ../lib/systemd/systemdsystemd is more than an init — it’s a system manager. What it does, in one line each:
- Builds boot order as a dependency graph and starts things in parallel
- Handles service (daemon) start / stop / restart / auto-recovery
- Manages other resources — mounts, swap, timers, sockets — under the same model
- Collects every child process’s logs into
journaldin one place
The old init relied on shell scripts under /etc/init.d/<service> start. Serial, slow, hard to debug. systemd is declarative: you describe “how this service should run” in a text file (a unit), and systemd handles the rest.
Types of units #
The basic unit systemd manages is a unit. Several flavors:
| Extension | Meaning | Example |
|---|---|---|
.service | A daemon or command (most common) | nginx.service, sshd.service |
.socket | Socket activation | cockpit.socket |
.target | A bundle of units (a group) | multi-user.target |
.timer | Time-based trigger (cron replacement) | dnf-makecache.timer |
.mount | Filesystem mount | home.mount |
.path | Watch for file changes | cups.path |
.slice / .scope | Resource group (cgroup) | user-1000.slice |
This post focuses on the two you’ll meet most: service and target. The rest just gets a quick mention.
systemctl — talking to systemd #
systemctl covers practically every systemd operation.
Status — status
#
$ systemctl status sshd
● sshd.service - OpenSSH server daemon
Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; preset: enabled)
Active: active (running) since Fri 2026-04-12 10:01:32 KST; 1h ago
Docs: man:sshd(8)
Main PID: 942 (sshd)
Tasks: 1 (limit: 4632)
Memory: 5.2M
CPU: 30ms
CGroup: /system.slice/sshd.service
└─942 "sshd: /usr/sbin/sshd -D ..."
Apr 12 10:01:32 rhel9-lab systemd[1]: Starting OpenSSH server daemon...
Apr 12 10:01:32 rhel9-lab sshd[942]: Server listening on 0.0.0.0 port 22.
Apr 12 10:01:32 rhel9-lab systemd[1]: Started OpenSSH server daemon.What status shows in one shot:
- Loaded — path to the unit file, and whether it’s enabled (auto-start on boot)
- Active — current state (
active (running),inactive (dead),failed…) - Main PID — PID of the main process
- CGroup — which cgroup the children belong to
- The last 10ish log lines — what just happened
When something’s wrong, systemctl status <service> is almost always the first command to run.
Start / stop / restart #
$ sudo systemctl start nginx # start
$ sudo systemctl stop nginx # stop
$ sudo systemctl restart nginx # restart
$ sudo systemctl reload nginx # reload config only (if supported)restart and reload are different things.
- restart — kills the process and brings it back up. Brief downtime.
- reload — same process receives something like SIGHUP and re-reads its config. No downtime. Not all services support it (nginx and sshd do, for instance).
In production, reload is safer if you only changed config; use restart when you also need the process to fully reset its in-memory state.
Auto-start on boot — enable / disable
#
$ sudo systemctl enable nginx # auto-start on boot
$ sudo systemctl disable nginx # turn off auto-start
$ sudo systemctl enable --now nginx # start now + enable on bootDon’t confuse enable and start — they do different things.
| Command | Right now | After reboot |
|---|---|---|
start | ✅ Started | ❌ Not started |
enable | ❌ Not started | ✅ Auto-starts |
enable --now | ✅ Started | ✅ Auto-starts |
--now is the flag your fingers will reach for. “Just-installed service: start it now and have it come up next boot too” — that’s almost always what you want.
Lists #
$ systemctl list-units --type=service # services currently in memory
$ systemctl list-unit-files --type=service # all service units on disk
$ systemctl list-units --state=failed # only failed services--state=failed is a great first step when chasing problems — any red lines right after boot?
target — the system’s “mode” #
systemd replaced the old init runlevels (0–6) with targets — a bundle of units that together form a “mode.”
| target | Old runlevel | Meaning |
|---|---|---|
poweroff.target | 0 | Power off |
rescue.target | 1 | Single-user rescue mode |
multi-user.target | 3 | Text multi-user (default for servers) |
graphical.target | 5 | Multi-user with GUI |
reboot.target | 6 | Reboot |
$ systemctl get-default
graphical.target
$ systemctl is-active graphical.target
activeChanging the default target #
A learning machine without GUI requirements gets noticeably lighter on memory and CPU when it boots into multi-user.target.
$ sudo systemctl set-default multi-user.target
Created symlink /etc/systemd/system/default.target → /usr/lib/systemd/system/multi-user.target.
$ sudo rebootTo get the GUI back, set-default graphical.target. To switch modes immediately without rebooting:
$ sudo systemctl isolate multi-user.target # to text mode now
$ sudo systemctl isolate graphical.target # back to GUIRescue / emergency modes #
Targets you’ll want when boot is broken. You enter them from the GRUB boot menu.
| target | What it is |
|---|---|
rescue.target | Single-user mode. Almost no services, root shell |
emergency.target | Even smaller. Filesystem mounted read-only |
In GRUB, press e to edit, append systemd.unit=rescue.target to the kernel line, then Ctrl+X to boot. Detailed boot recovery is in Advanced #1.
Writing your first .service unit
#
Knowing how to handle services someone else installed is only half of systemd. Writing a unit yourself once makes the whole model click.
A toy script #
First, a tiny script for systemd to run.
#!/bin/bash
while true; do
echo "[$(date +%H:%M:%S)] Hello from systemd"
sleep 5
done$ sudo chmod +x /usr/local/bin/hello-loop.shThe unit file #
[Unit]
Description=Hello loop demo
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/hello-loop.sh
Restart=on-failure
RestartSec=2
User=nobody
[Install]
WantedBy=multi-user.targetThree sections.
[Unit] — metadata about the unit itself.
Description— the one-liner shown insystemctl statusAfter— units that should be running before this one starts. A dependency. (Hard requirement:Requires)
[Service] — how to run it.
Type=simple— the most common form. WhateverExecStartruns is the main process. (Other types:forking,oneshot,notify, …)ExecStart— the command to run. Use an absolute path.Restart=on-failure— auto-restart on abnormal exit. (always/no/on-successare also options.)RestartSec— interval between restarts (seconds)User=nobody— which user to run as. Drops privileges away from root by default.
[Install] — what enable should do.
WantedBy=multi-user.target— start when this target is activated. Without this line,enabledoes nothing.
Activate it #
$ sudo systemctl daemon-reload # let systemd pick up the new unit file
$ sudo systemctl enable --now hello-loop
Created symlink ...
$ systemctl status hello-loop
● hello-loop.service - Hello loop demo
Loaded: loaded (/etc/systemd/system/hello-loop.service; enabled; ...)
Active: active (running) since ...
Main PID: 12345 (hello-loop.sh)
...
CGroup: /system.slice/hello-loop.service
├─12345 /bin/bash /usr/local/bin/hello-loop.sh
└─12389 sleep 5Watch out — forgetting
daemon-reloadis the most common mistake. After creating or editing a unit file, alwaysdaemon-reload. Otherwise systemd keeps using the old definition.
Where you put the unit file matters.
| Path | Used for |
|---|---|
/usr/lib/systemd/system/ | Units installed by packages (don’t edit directly) |
/etc/systemd/system/ | Your own / overriding units |
~/.config/systemd/user/ | Per-user units (user systemd) |
When the same name exists in both, /etc/systemd/system/ wins. To override only part of a package’s unit, use systemctl edit <service> — it creates an override file in the drop-in directory.
$ sudo systemctl edit nginx
# editor opens /etc/systemd/system/nginx.service.d/override.conf
[Service]
Environment="NGINX_OPTS=-q"
LimitNOFILE=65536You can layer config on top without touching the original. When package updates refresh the original, your override survives.
journalctl — where every log lands #
systemd ships with a logging daemon called journald. Every unit’s stdout/stderr, kernel messages, syslog — everything ends up in one place.
$ journalctl # everything (Page Down to scroll)
$ journalctl -e # jump to the end (most recent)
$ journalctl -f # live follow (like tail -f)
$ journalctl -n 100 # last 100 linesFilters #
$ journalctl -u nginx # only nginx unit's logs
$ journalctl -u nginx -f # nginx live
$ journalctl -b # this boot only
$ journalctl -b -1 # the previous boot
$ journalctl --list-boots # list of retained boots
$ journalctl --since "2026-04-12 10:00" --until "2026-04-12 11:00"
$ journalctl --since "1 hour ago"
$ journalctl --since today
$ journalctl -p err # priority err and above
$ journalctl -p warning..err # rangeUseful combinations #
$ journalctl -u sshd -f # follow sshd in real time
$ journalctl -u nginx --since today -p err # today's errors only
$ journalctl _PID=12345 # logs for a specific PID
$ journalctl _COMM=sudo # the sudo command's own logThe filter keys like _PID, _COMM, and _UID are metadata journald fills in automatically. Run journalctl -F _COMM to see what values are available.
journald persistence #
By default journald is volatile — old logs disappear after reboot. Flip one switch to make it persistent:
$ sudo mkdir -p /var/log/journal
$ sudo systemd-tmpfiles --create --prefix /var/log/journal
$ sudo systemctl restart systemd-journaldIf journalctl --list-boots now lists multiple boots, persistence is on. Cap disk usage in /etc/systemd/journald.conf with values like SystemMaxUse=1G.
Where did /etc/init.d/ go?
#
Old material talks about service nginx start or /etc/init.d/nginx start. On RHEL 9 those still work, but they’re translated to systemctl under the hood.
$ sudo service nginx status # turns into systemctl status nginx
$ sudo chkconfig nginx on # turns into systemctl enable nginxIf you’re learning fresh, go straight to systemctl. The old names are worth knowing only when you read old material.
Common commands at a glance #
| Command | What it does |
|---|---|
systemctl status <unit> | Status + recent logs |
systemctl start/stop/restart/reload <unit> | Lifecycle |
systemctl enable [--now] <unit> | Auto-start on boot (+ start now) |
systemctl disable [--now] <unit> | Disable auto-start (+ stop now) |
systemctl is-active/is-enabled <unit> | One-word answer |
systemctl list-units --state=failed | Failed units only |
systemctl daemon-reload | Re-read units after editing |
systemctl get-default / set-default | View / change default target |
systemctl isolate <target> | Switch target right now |
systemctl edit <unit> | Edit a drop-in override |
journalctl -u <unit> [-f] | Logs for that unit (+ live) |
journalctl -b [-1] | Logs from this / previous boot |
journalctl --since "..." --until "..." | Time range |
journalctl -p err | Priority filter |
Common pitfalls #
“Unit not found” #
Created a new unit file but systemctl start says “Unit not found”? Almost always a missing daemon-reload. Putting the file on disk doesn’t make systemd pick it up automatically.
“I enabled it but it doesn’t come up after reboot” #
The unit is missing the [Install] section. Without WantedBy=multi-user.target, enable has nothing to act on. If systemctl enable complains about “no installation config,” that’s the cause.
“Active: failed” #
The fastest way to see what went wrong is journalctl -u <unit> -e. It shows the error that killed the service right at startup.
Common causes:
ExecStartuses a relative path (must be absolute)- The executable lacks the executable bit (
chmod +x) - The user in
User=can’t access required files (permissions) Type=doesn’t match the actual behavior
“Too many logs” #
Plain journalctl dumps everything since boot. Almost always pair it with -u <unit>, -b, or --since.
Wrap-up #
Picture from this post:
- systemd is RHEL’s PID 1 — the manager holding the whole boot/services chain.
- A unit is the basic thing systemd manages. service, target, timer, mount, etc.
systemctl status / start / stop / restart / reload / enable / disableare the daily commands.enable --nowis the one your fingers reach for most.- A target replaces the old runlevel.
multi-user.target(text) /graphical.target(GUI) /rescue.target(rescue). - A hand-written
.servicegoes in/etc/systemd/system/, thendaemon-reload+enable --now. - Use
systemctl editto layer drop-in overrides without touching package units. - journalctl is where every log lands — narrow with
-u,-b,--since,-p,-f.
Next — users and permissions #
Which user systemd runs a service as is decided by the unit’s User= line. How that user exists in the system, and which files they can touch, is the topic of the next post.
#5 Users / groups / permissions — UID/GID, sudo, ACL covers /etc/passwd and /etc/shadow, the useradd / usermod / groupadd family, the meaning of rwx and the two notations of chmod, ACL (getfacl/setfacl) for finer-grained control, and sudo with /etc/sudoers.d/ — all in one post.