RHEL Basics #4: Intro to systemd — Services, Targets, journalctl

11 min read

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:

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.

check PID 1
$ ps -p 1
   PID TTY          TIME CMD
     1 ?        00:00:01 systemd

$ ls -l /sbin/init
lrwxrwxrwx. 1 root root ... /sbin/init -> ../lib/systemd/systemd

systemd 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 journald in 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:

ExtensionMeaningExample
.serviceA daemon or command (most common)nginx.service, sshd.service
.socketSocket activationcockpit.socket
.targetA bundle of units (a group)multi-user.target
.timerTime-based trigger (cron replacement)dnf-makecache.timer
.mountFilesystem mounthome.mount
.pathWatch for file changescups.path
.slice / .scopeResource 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 #

service 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 #

lifecycle
$ 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 #

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 boot

Don’t confuse enable and start — they do different things.

CommandRight nowAfter 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 #

list-units / list-unit-files
$ 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.”

targetOld runlevelMeaning
poweroff.target0Power off
rescue.target1Single-user rescue mode
multi-user.target3Text multi-user (default for servers)
graphical.target5Multi-user with GUI
reboot.target6Reboot
current target / default target
$ systemctl get-default
graphical.target

$ systemctl is-active graphical.target
active

Changing the default target #

A learning machine without GUI requirements gets noticeably lighter on memory and CPU when it boots into multi-user.target.

boot to text mode by default
$ sudo systemctl set-default multi-user.target
Created symlink /etc/systemd/system/default.target → /usr/lib/systemd/system/multi-user.target.

$ sudo reboot

To get the GUI back, set-default graphical.target. To switch modes immediately without rebooting:

immediate switch (one-shot)
$ sudo systemctl isolate multi-user.target     # to text mode now
$ sudo systemctl isolate graphical.target      # back to GUI

Rescue / emergency modes #

Targets you’ll want when boot is broken. You enter them from the GRUB boot menu.

targetWhat it is
rescue.targetSingle-user mode. Almost no services, root shell
emergency.targetEven 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.

/usr/local/bin/hello-loop.sh
#!/bin/bash
while true; do
    echo "[$(date +%H:%M:%S)] Hello from systemd"
    sleep 5
done
executable bit
$ sudo chmod +x /usr/local/bin/hello-loop.sh

The unit file #

/etc/systemd/system/hello-loop.service
[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.target

Three sections.

[Unit] — metadata about the unit itself.

  • Description — the one-liner shown in systemctl status
  • After — 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. Whatever ExecStart runs 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-success are 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, enable does nothing.

Activate it #

register / start / verify
$ 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 5

Watch out — forgetting daemon-reload is the most common mistake. After creating or editing a unit file, always daemon-reload. Otherwise systemd keeps using the old definition.

Where you put the unit file matters.

PathUsed 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.

drop-in override
$ sudo systemctl edit nginx
# editor opens /etc/systemd/system/nginx.service.d/override.conf
[Service]
Environment="NGINX_OPTS=-q"
LimitNOFILE=65536

You 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.

basics
$ 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 lines

Filters #

unit / boot / time / priority
$ 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           # range

Useful combinations #

real life
$ 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 log

The 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:

enable persistence
$ sudo mkdir -p /var/log/journal
$ sudo systemd-tmpfiles --create --prefix /var/log/journal
$ sudo systemctl restart systemd-journald

If 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.

legacy commands (compatibility)
$ sudo service nginx status        # turns into systemctl status nginx
$ sudo chkconfig nginx on          # turns into systemctl enable nginx

If 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 #

CommandWhat 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=failedFailed units only
systemctl daemon-reloadRe-read units after editing
systemctl get-default / set-defaultView / 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 errPriority 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:

  • ExecStart uses 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 / disable are the daily commands. enable --now is 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 .service goes in /etc/systemd/system/, then daemon-reload + enable --now.
  • Use systemctl edit to 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.

X